DEV Community

Dmitry Strugovshchikov
Dmitry Strugovshchikov

Posted on Originally published at github.com Fully Autonomous

Guardrails for autonomous coding agents: five hooks that make Claude Code harder to trust falsely

I run a lot of my day-to-day work through autonomous coding agents. Not "ask a question, copy the answer" — actual unattended runs where an agent edits files, runs commands, and is supposed to come back when the job is done.

The first thing you learn doing this at any scale is that the failure modes aren't the dramatic ones. The agent rarely does something catastrophic. What it does, constantly, is small dishonesty and small drift: it says "all done" when two tasks are still open. It writes an API token into a file because that was the path of least resistance. It re-runs the same failing command with a slightly different flag, six times, instead of stopping to rethink. It quietly appends to a context file until the file is too big and the session falls over.

None of these are model failures, exactly. They're judgment-in-the-moment failures — and you cannot fix judgment-in-the-moment by asking the model to be more careful. You fix it by constraining the actions that matter, deterministically, at the boundary where the agent acts.

For context: these five are extracted from a larger setup that runs a small e-commerce business I own, where agents do most of the day-to-day work unattended: a few dozen hooks and a couple of hundred scheduled jobs. The five below are the ones I would install first on any machine, because each one closed a failure I kept paying for.

That's what guardrails are. Here are five I rely on, why each exists, and the mechanism that makes them cheap to run.

How Claude Code hooks work (the 30-second version)

Claude Code can run a script at lifecycle events: before a tool runs (PreToolUse), after a tool runs (PostToolUse), when you submit a prompt (UserPromptSubmit), and when a turn ends (Stop). The script gets a JSON event on stdin, and its exit code decides what happens:

  • 0 -> allow / no-op
  • 2 -> block the action; whatever the script prints to stderr is fed back to the model so it can correct course

That's the whole contract. A guardrail is just a small program that reads an event and decides: allow, or block-with-an-explanation. No framework, no dependencies — for me, plain Python from the standard library.

The important design rule: fail open. If a hook hits an unexpected input or throws, it must exit 0. A guardrail should never be the reason your session breaks. It's there to catch a specific bad action, not to become a new point of failure.

1. Block the false "all done"

The single most corrosive agent behavior is claiming completion that isn't true. Once you can't trust "done," you have to re-verify everything, and the agent's autonomy is worthless.

This guardrail runs on Stop. It reads the transcript and the agent's own latest task list. If the final message reads as a completion claim ("all done", "everything is complete", "session finished") but there are still pending or in-progress tasks, it blocks:

GUARDRAIL: completion claimed, but 2 todo(s) are still open:
     [pending    ] Wire up the retry path
     [in_progress] Add the integration test
Enter fullscreen mode Exit fullscreen mode

Crucially, it has an escape hatch: if the message also honestly acknowledges remaining work ("remaining:", "next steps", "not done yet"), it allows it. The goal isn't to forbid summaries — it's to forbid dishonest ones. Honesty about unfinished work passes freely; a clean "all done" over an open task list does not.

2. Block secrets before they hit disk

A token written into a tracked file is not a recoverable mistake. It's in git history, in logs, in transcripts — places you can't fully scrub. The only reliable interception point is before the write.

This one runs on PreToolUse for Write/Edit. It scans the content about to be written for high-confidence secret signatures — provider API keys, OAuth tokens, private key blocks, JWTs — and blocks if it finds one. It never prints the secret back, only its type and length:

GUARDRAIL: content looks like it contains a secret:
   - GitHub personal access token: ghp_a...(40 chars)
Enter fullscreen mode Exit fullscreen mode

The patterns are deliberately high-signal (specific prefixes, minimum lengths) to keep false positives near zero, and there's a path allowlist for the legitimate cases — .env.example templates, test fixtures.

3. Cap the files that get loaded into context

Some files are read into the model's context every turn — long-lived memory files, rolling notes, injected context. If one grows without bound, it bloats context, slows everything down, and in some setups breaks the session outright. I learned this the hard way: a memory file crossed a size threshold and every single turn started dying on a connection error until I pruned it.

So: a PreToolUse guard on Write/Edit that, for a configured set of watched files, computes the resulting size of the operation and blocks if it would cross the limit. It forces you to prune instead of appending forever. The watched files and limits live in config — point it at whatever your setup loads into context.

4 & 5. Stop the blind retry loop

Two failures on the same target usually means the approach is wrong — not that it needs another tweak. But an agent left to its own devices will happily tweak-and-retry well past the point of diminishing returns. This is the most expensive failure mode in wall-clock terms: long stretches of motion without progress.

This takes two cooperating hooks:

  • A PostToolUse hook on Bash that records failures. It extracts a "target" from each command (a filename, a URL host, else the first significant word) so that retries of the same thing are grouped, and keeps a per-session counter: increment on failure, reset on success.
  • A PreToolUse hook on Bash that, before a command runs, checks that counter and blocks once a target has failed twice in a row:
GUARDRAIL: 2 consecutive failures on 'deploy.sh' (last failure 3 min ago).
   Two strikes on the same target usually means the approach is wrong,
   not that it needs another tweak. Stop and re-think:
     - get fresh diagnostics (logs, error output, a smaller repro)
     - change strategy rather than re-running a variant
Enter fullscreen mode Exit fullscreen mode

The state expires after a couple of hours, so a target you genuinely fixed and come back to later starts clean.

(There's a sixth, lighter one I run too: on UserPromptSubmit, when I ask for approval of something, it reminds the agent to state the consequences first — what it'll do, what happens if I decline, whether it's reversible, the top risks. Cheap, and it turns a vague "ok?" into an informed decision.)

What I've learned encoding rules this way

Determinism beats good intentions. Every one of these rules is something I could ask the agent to do. Asking works 90% of the time. The 10% is exactly when it matters — under ambiguity, under time pressure, deep in a loop. A four-line Python check that runs every time doesn't have a bad day.

The interception point is everything. Catching a secret after it's written, or a false "done" after you've acted on it, is too late. Guardrails earn their keep by sitting before the action that's hard to undo.

Guardrails should be humble. Fail open, never block on your own bug, always print a clear path to correct. A guardrail that's annoying or fragile gets disabled, and then it protects nothing.

This is the same shape as deployment safety in any production LLM system: you don't hope the model behaves — you constrain the actions that matter and leave everything else free.

The five hooks are open source, dependency-free, and documented per-hook, with a test suite that runs each one as a real subprocess: github.com/dmitry-strugovshchikov/claude-code-guardrails. Adopt one, several, or all of them.

If you're running agents unattended and have hit your own version of these failure modes, I'd genuinely like to hear which guardrails you reached for.

Top comments (0)