DEV Community

Cover image for I Built 11 Claude Code Hooks. Six Were Dead Within 24 Hours.
Phil Rentier Digital
Phil Rentier Digital

Posted on • Originally published at rentierdigital.xyz

I Built 11 Claude Code Hooks. Six Were Dead Within 24 Hours.

I had one hook on Claude Code. One. A TypeScript checker that ran npx tsc in a project that uses Bun, and piped the output through head -20. So when there were 12 errors, I saw three. Great system.

Meanwhile, my agent could force-push to main, leak my API keys, and rm -rf the repo without anything stopping it. Where I lost it was when it deployed to prod on the SaaS. I had explicitly told it not to. It did it anyway.

I write about Claude Code best practices. My own setup had none.

So I did what every developer does when they realize their safety net is made of cardboard: I built eleven hooks at once. Three days later, six were disabled. The other five, I forgot they existed. That's exactly how it should work if you want to code (without reading your own code) and sleep at night. Almost.

TLDR: The hooks that survive are the ones you forget about (security, secrets, destructive ops). The hooks that die are the ones that nag you on every edit. I audited mine over three days, killed 6 out of 11, and the 5 survivors already caught 2 near-leaks and a force-push. Audit yours tonight. Takes 20 minutes.

The Single Hook That Was Supposed to Save Me

Before the audit, my entire hook setup was one PostToolUse trigger on Edit and Write operations. It ran npx tsc to check TypeScript errors after every file change. Two problems with that.

First, the project runs on Bun. Running npx in a Bun project is like asking your French neighbor to proofread your Japanese homework. It kinda works. Sometimes. When the stars align. Second, I piped through head -20, which cuts the output. TypeScript screams about 12 broken types, I see the first 3, Claude sees the first 3, and we both nod and move on. My type checker wasn't checking types.

But that's the funny part. That broken hook was the only thing standing between my agent and full access to everything. No guard on git push --force. No block on rm -rf. No secret scanning. No deploy protection. The agent could nuke the repo, push to main, and leak every API key in the project. The only thing it couldn't do (in theory) was write TypeScript that doesn't compile. And even that, it was doing wrong.

I had structured my CLAUDE.md like a real config file, with rules, with boundaries, with four lines of integrity principles. But CLAUDE.md is a suggestion. The agent reads it, considers it, and sometimes decides it knows better. A hook is a wall. Exit code 2, tool call blocked, no negotiation.

The wall existed. It was just broken.

My safety net was a type checker that couldn't check types.

5,000 Likes for No Guardrails

On March 31st, someone published a fork of Claude Code. The source code had leaked through the npm package a few days earlier (Fortune and CNBC covered it, it's public record). The fork stripped the safety guardrails from the prompts, removed telemetry, unlocked experimental features, and got uploaded to IPFS. It got roughly five times the traction of a solid educational thread about hooks that a developer named Akshay posted the same week.

That ratio tells you something, and it's not that developers hate safety.

Developers hate noise.

Think about it. You install a lint hook that fires on every file edit. You install a test runner that adds 4 to 8 seconds per save during a 15-file refactor. You install a context warning that pops up every 20 minutes telling you the conversation is getting long (yes, I know, I'm the one having it). After a day of this, you start ignoring all hook output. Every notification. Every warning. Including the real ones.

The kitchen smoke detector problem. The thing beeps every time you make toast. So you rip the battery out. And the one night there's an actual fire, no detector.

That's what the traction gap is about. Not "we don't want guardrails." More like "we don't want your guardrails, the ones that interrupt us 47 times a day to say nothing useful."

The conclusion of the free-code project is wrong, though. Zero guardrails isn't the answer. The answer is killing the six that make noise and keeping the five that save you.

I haven't used free-code myself. I'm reading those numbers as market signal, not endorsement. But the signal is clear: when you make developers choose between noisy safety and silence, they pick silence every time.

3 Days, 11 Hooks, 6 Body Bags

I'll spare you the hook-by-hook walkthrough. (You're welcome.) What matters is the pattern, and it was obvious by day two: the hooks that survived are invisible, the hooks that died were loud.

The five that lived:

The TypeScript checker was a 30-second fix. Swap npx for bunx, swap head -20 for tail -5 (you want the last errors, not the first, because the first ones are usually cascading noise from one real break). Immediately caught a type error Claude had introduced three edits earlier.

The git guard was the one I should have built first. PreToolUse on Bash, pattern-matches against push --force, push.*-f, and reset --hard. Day one, it blocked a real force-push. Claude was trying to fix a merge conflict by brute-forcing the remote. Had it gone through, it would have erased a commit I actually needed. I merged the dangerous-ops patterns into this same hook (rm -rf, chmod 777, curl | bash). One hook, one regex list, every operation where "oops" is permanent.

The secret scanner runs PreToolUse on Edit and Write. Regex for API key patterns, bearer tokens, base64-encoded credentials. In three days, it flagged twice: a bearer token hardcoded in a test file, and a console.log that dumped a complete auth header. Both would have been committed without a sound. These are the same kind of secrets I'd found sitting in cleartext in .claude/settings.local.json a few weeks earlier. The scanner catches them before they hit the repo.

The deploy guard and the MCP call logger round out the five. Deploy guard: zero triggers in three days, which is the entire point (you don't judge a seatbelt by how often it fires). MCP logger: revealed Claude was fetching the same resource four times per conversation. Not a safety tool, a context window diagnostic. Both stay because they cost zero friction.

The six that died:

Noise. Three of them. The test runner added 4 to 8 seconds per file save. During a refactor touching 15 files, that's two minutes staring at the terminal. Disabled after half a day. The lint hook was redundant with the TypeScript checker (same errors, different format, double the interruptions). The context window warning fired every 20 minutes. After the tenth time, I was trained to ignore every hook output. That last one is the most dangerous thing a hook can do.

Uselessness. Two of them. The bundle size checker measured the wrong directory for the wrong build tool. The commit message enforcer rejected "generic" messages, but Claude already writes decent commit messages by default. Solving a problem that doesn't exist anymore.

Redundancy. One. A separate dangerous-commands hook that matched the same patterns as the git guard. Same regex, same exit code, two hooks doing one job. Merged and deleted.

Six down. Five standing.

A Good Hook Is One You Forget Exists

Here's what separates the survivors from the dead: the survivors never made me think about them. They run on every tool call, check what they check, stay quiet when things are fine. I only notice them when they block something. And when they block something, it's always something I'm glad got blocked.

The dead hooks did the opposite. They interrupted, slowed things down, nagged. And after enough of that, I disabled them. That's when the real problem starts.

A disabled hook is worse than no hook. Because you still think you're protected. The secret scanner is running, right? Well no, you turned it off last Tuesday when you got fed up with the lint noise, and you forgot the scanner was in the same config.

This is the failure mode nobody talks about. Bad hooks don't just fail. They poison the good ones. One noisy hook trains you to ignore all hook output, and that includes the one that just caught your production database credentials in a console.log.

There's a scale to this. The four integrity lines in my CLAUDE.md ("Never lie, Never hide, Never conceal, Never fail silently") changed how the agent behaves, but they remain suggestions that an agent under pressure can step around. When the agent optimizes for "done," it treats CLAUDE.md rules as advisory. A hook doesn't negotiate. Exit code 2, operation blocked. Period.

But hooks don't replace CLAUDE.md. They cover the operations where failure is irreversible: a leaked secret, a force-push, an accidental deploy. For everything else (code style, architecture decisions, naming conventions), CLAUDE.md is the right tool. You don't build a brick wall to enforce variable naming.

The things that need a wall, though, really need a wall.

The Gap

Before the audit: one hook. Broken. Zero security. The agent could deploy, delete, and leak without making a sound.

After: five hooks. Working. Covering the three things that actually matter (type safety, secrets, destructive operations). Silent when things are fine. Loud when they're not.

The gap between "I write about Claude Code best practices" and "I follow Claude Code best practices" was exactly four hooks wide.

What do yours look like?

If you audit your own hooks after reading this, I want to know what you find. The worst discoveries make the best stories 😅

Sources

Akshay's thread on Claude Code hooks lifecycle and configuration (March 2025, X/Twitter).

(*) The cover is AI-generated. The hooks in the image look clean because they've never seen a real codebase.

Top comments (0)