<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dmitry Strugovshchikov</title>
    <description>The latest articles on DEV Community by Dmitry Strugovshchikov (@dstrugovshchikov).</description>
    <link>https://dev.to/dstrugovshchikov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4132848%2F41a97c59-6f9a-4b06-8b09-e15632ec4299.png</url>
      <title>DEV Community: Dmitry Strugovshchikov</title>
      <link>https://dev.to/dstrugovshchikov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dstrugovshchikov"/>
    <language>en</language>
    <item>
      <title>Guardrails for autonomous coding agents: five hooks that make Claude Code harder to trust falsely</title>
      <dc:creator>Dmitry Strugovshchikov</dc:creator>
      <pubDate>Sat, 19 Sep 2026 11:55:23 +0000</pubDate>
      <link>https://dev.to/dstrugovshchikov/guardrails-for-autonomous-coding-agents-five-hooks-that-make-claude-code-harder-to-trust-falsely-3hpj</link>
      <guid>https://dev.to/dstrugovshchikov/guardrails-for-autonomous-coding-agents-five-hooks-that-make-claude-code-harder-to-trust-falsely-3hpj</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;small dishonesty and small drift&lt;/em&gt;: 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.&lt;/p&gt;

&lt;p&gt;None of these are model failures, exactly. They're &lt;strong&gt;judgment-in-the-moment failures&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How Claude Code hooks work (the 30-second version)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; can run a script at lifecycle events: before a tool runs (&lt;code&gt;PreToolUse&lt;/code&gt;), after a tool runs (&lt;code&gt;PostToolUse&lt;/code&gt;), when you submit a prompt (&lt;code&gt;UserPromptSubmit&lt;/code&gt;), and when a turn ends (&lt;code&gt;Stop&lt;/code&gt;). The script gets a JSON event on &lt;strong&gt;stdin&lt;/strong&gt;, and its &lt;strong&gt;exit code&lt;/strong&gt; decides what happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; -&amp;gt; allow / no-op&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt; -&amp;gt; block the action; whatever the script prints to &lt;strong&gt;stderr&lt;/strong&gt; is fed back to the model so it can correct course&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The important design rule: &lt;strong&gt;fail open&lt;/strong&gt;. If a hook hits an unexpected input or throws, it must exit &lt;code&gt;0&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Block the false "all done"
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This guardrail runs on &lt;code&gt;Stop&lt;/code&gt;. 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") &lt;strong&gt;but&lt;/strong&gt; there are still pending or in-progress tasks, it blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GUARDRAIL: completion claimed, but 2 todo(s) are still open:
     [pending    ] Wire up the retry path
     [in_progress] Add the integration test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  2. Block secrets before they hit disk
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;before the write&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This one runs on &lt;code&gt;PreToolUse&lt;/code&gt; for &lt;code&gt;Write&lt;/code&gt;/&lt;code&gt;Edit&lt;/code&gt;. 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GUARDRAIL: content looks like it contains a secret:
   - GitHub personal access token: ghp_a...(40 chars)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 — &lt;code&gt;.env.example&lt;/code&gt; templates, test fixtures.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Cap the files that get loaded into context
&lt;/h2&gt;

&lt;p&gt;Some files are read into the model's context &lt;em&gt;every turn&lt;/em&gt; — 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.&lt;/p&gt;

&lt;p&gt;So: a &lt;code&gt;PreToolUse&lt;/code&gt; guard on &lt;code&gt;Write&lt;/code&gt;/&lt;code&gt;Edit&lt;/code&gt; that, for a configured set of watched files, computes the &lt;em&gt;resulting&lt;/em&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  4 &amp;amp; 5. Stop the blind retry loop
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This takes two cooperating hooks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;PostToolUse&lt;/code&gt; hook on &lt;code&gt;Bash&lt;/code&gt; 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 &lt;em&gt;same&lt;/em&gt; thing are grouped, and keeps a per-session counter: increment on failure, reset on success.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;PreToolUse&lt;/code&gt; hook on &lt;code&gt;Bash&lt;/code&gt; that, before a command runs, checks that counter and &lt;strong&gt;blocks once a target has failed twice in a row&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GUARDRAIL: 2 consecutive failures on &lt;span class="s1"&gt;'deploy.sh'&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;last failure 3 min ago&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
   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 &lt;span class="o"&gt;(&lt;/span&gt;logs, error output, a smaller repro&lt;span class="o"&gt;)&lt;/span&gt;
     - change strategy rather than re-running a variant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state expires after a couple of hours, so a target you genuinely fixed and come back to later starts clean.&lt;/p&gt;

&lt;p&gt;(There's a sixth, lighter one I run too: on &lt;code&gt;UserPromptSubmit&lt;/code&gt;, 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.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've learned encoding rules this way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Determinism beats good intentions.&lt;/strong&gt; Every one of these rules is something I could &lt;em&gt;ask&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The interception point is everything.&lt;/strong&gt; 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 &lt;em&gt;before&lt;/em&gt; the action that's hard to undo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guardrails should be humble.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The five hooks are open source, dependency-free, and documented per-hook, with a test suite that runs each one as a real subprocess: &lt;strong&gt;&lt;a href="https://github.com/dmitry-strugovshchikov/claude-code-guardrails" rel="noopener noreferrer"&gt;github.com/dmitry-strugovshchikov/claude-code-guardrails&lt;/a&gt;&lt;/strong&gt;. Adopt one, several, or all of them.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>agents</category>
      <category>python</category>
    </item>
  </channel>
</rss>
