<?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: Rabih Jabr</title>
    <description>The latest articles on DEV Community by Rabih Jabr (@rabih_jabr_29).</description>
    <link>https://dev.to/rabih_jabr_29</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%2F4087038%2F938d220e-820b-4f78-a341-97b72aa74f5e.jpg</url>
      <title>DEV Community: Rabih Jabr</title>
      <link>https://dev.to/rabih_jabr_29</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rabih_jabr_29"/>
    <language>en</language>
    <item>
      <title>Your agent isn't reckless. It just can't see the blast radius.</title>
      <dc:creator>Rabih Jabr</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:29:21 +0000</pubDate>
      <link>https://dev.to/rabih_jabr_29/your-agent-isnt-reckless-it-just-cant-see-the-blast-radius-1lkj</link>
      <guid>https://dev.to/rabih_jabr_29/your-agent-isnt-reckless-it-just-cant-see-the-blast-radius-1lkj</guid>
      <description>&lt;p&gt;I've been running Claude Code as a daily driver for about three months now. It writes&lt;br&gt;
Ansible I'd have taken a week to write. It reads a codebase faster than I do. It is, genuinely, very good.&lt;/p&gt;

&lt;p&gt;It also once wanted to force-push to &lt;code&gt;main&lt;/code&gt;, and it wanted to for an extremely good&lt;br&gt;
reason.&lt;/p&gt;

&lt;p&gt;Sit with that for a second, because it's the whole post.&lt;/p&gt;

&lt;p&gt;The rebase was stuck. Force-pushing would have unstuck it. Every link in that chain of&lt;br&gt;
reasoning is sound. The agent wasn't being careless, wasn't hallucinating, wasn't&lt;br&gt;
"drifting" or whatever we're calling it this month. It made a locally correct decision&lt;br&gt;
with a non-local consequence, which is the exact category of mistake that human code&lt;br&gt;
review is worst at catching — because the diff looks &lt;em&gt;fine&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It could see the command. It could not see the crater.&lt;/p&gt;
&lt;h2&gt;
  
  
  The thing I stopped doing
&lt;/h2&gt;

&lt;p&gt;For a while my answer was to read everything. Every diff, every command, eyes on the&lt;br&gt;
screen, hand hovering over Ctrl-C like a man watching a toddler near a staircase.&lt;/p&gt;

&lt;p&gt;This does not scale, and the reason it doesn't is embarrassing when you say it out loud:&lt;br&gt;
&lt;strong&gt;reviewing output scales with how much the agent writes.&lt;/strong&gt; That number is going exactly&lt;br&gt;
one direction, and it isn't down.&lt;/p&gt;

&lt;p&gt;So I flipped it. Instead of reviewing what it produces, I started writing down what it&lt;br&gt;
must never do.&lt;/p&gt;

&lt;p&gt;And here's the good news that took me way too long to notice: that list is &lt;em&gt;short&lt;/em&gt;. Not&lt;br&gt;
"short for a security policy" short. Short like you can fit it on a napkin.&lt;/p&gt;

&lt;p&gt;Here's mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A credential it read an hour ago gets inlined into a source file.&lt;/li&gt;
&lt;li&gt;A rebase gets stuck, and the fastest route to a green terminal is &lt;code&gt;git push --force origin main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rm -rf "$BUILD_DIR/"&lt;/code&gt; runs on the one machine where &lt;code&gt;BUILD_DIR&lt;/code&gt; never got set.&lt;/li&gt;
&lt;li&gt;A version bump gets typed straight into &lt;code&gt;package-lock.json&lt;/code&gt;, because that's the file the version number is visibly in.&lt;/li&gt;
&lt;li&gt;A failing test quietly grows a &lt;code&gt;.skip&lt;/code&gt; and CI goes green.&lt;/li&gt;
&lt;li&gt;Someone runs &lt;code&gt;cat .env&lt;/code&gt; "just to see which variables exist."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is my favourite, and I'll come back to it.&lt;/p&gt;

&lt;p&gt;None of these are the agent being stupid. Every single one is a reasonable move by&lt;br&gt;
something that can't see two feet past the command it's about to run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Claude Code will let you say no
&lt;/h2&gt;

&lt;p&gt;This is the part I think a lot of people don't know exists.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PreToolUse&lt;/code&gt; is a hook that fires &lt;em&gt;before&lt;/em&gt; any tool call. Your script gets the whole&lt;br&gt;
thing on stdin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/rabih/app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hook_event_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git push --force origin main"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you can refuse it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hookSpecificOutput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hookEventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecisionReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This force-pushes to `main`, a shared branch."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the bit that genuinely surprised me.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;permissionDecisionReason&lt;/code&gt; string? The agent &lt;em&gt;reads&lt;/em&gt; it. And acts on it.&lt;/p&gt;

&lt;p&gt;Say "blocked" and it shrugs and retries with slightly different syntax, like a cat testing&lt;br&gt;
a closed door. Say "change the manifest and run &lt;code&gt;pnpm add&lt;/code&gt;" and it goes and does that,&lt;br&gt;
first try, no argument.&lt;/p&gt;

&lt;p&gt;Which reframes the whole thing. A denial isn't just a fence. It's the highest&lt;br&gt;
signal-to-noise teaching moment you will ever get, because it lands at the precise second&lt;br&gt;
the agent was about to be wrong. Nobody reads documentation at that moment. Everybody&lt;br&gt;
reads an error.&lt;/p&gt;

&lt;p&gt;So every guard I wrote has to answer two questions, not one: what's wrong, and what to do&lt;br&gt;
instead.&lt;/p&gt;
&lt;h2&gt;
  
  
  Thirteen of them
&lt;/h2&gt;

&lt;p&gt;They live here: &lt;strong&gt;&lt;a href="https://github.com/RabihJabr29/claude-guardrails" rel="noopener noreferrer"&gt;claude-guardrails&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Guard&lt;/th&gt;
&lt;th&gt;Blocks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;secrets-never-land-in-source&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Credential-shaped literals written into source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;secret-files-stay-out-of-context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reading &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;*.pem&lt;/code&gt;, &lt;code&gt;~/.aws/credentials&lt;/code&gt; into the session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;secrets-are-not-staged&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git add -A&lt;/code&gt; in a repo where &lt;code&gt;.env&lt;/code&gt; was never gitignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shared-branches-are-not-rewritten&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git push --force&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;release/*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uncommitted-work-is-not-discarded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git reset --hard&lt;/code&gt;, &lt;code&gt;git clean -fd&lt;/code&gt;, &lt;code&gt;git stash drop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verification-hooks-are-not-bypassed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--no-verify&lt;/code&gt;, &lt;code&gt;HUSKY=0&lt;/code&gt;, &lt;code&gt;--no-gpg-sign&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unexpanded-variables-in-destructive-paths&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rm -rf "$DIR/"&lt;/code&gt; where &lt;code&gt;$DIR&lt;/code&gt; could be empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;remote-code-is-not-piped-to-a-shell&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;`curl … \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{% raw %}&lt;code&gt;committed-migrations-are-immutable&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Editing a migration that's already committed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;destructive-sql-needs-a-where&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unbounded &lt;code&gt;DELETE&lt;/code&gt;/&lt;code&gt;UPDATE&lt;/code&gt;, ad-hoc &lt;code&gt;TRUNCATE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cluster-targets-are-explicit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Destructive &lt;code&gt;kubectl&lt;/code&gt; with no &lt;code&gt;--context&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tests-are-not-silenced&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Introducing &lt;code&gt;.skip&lt;/code&gt;, &lt;code&gt;@Disabled&lt;/code&gt;, &lt;code&gt;continue-on-error: true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lockfiles-are-generated-not-edited&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hand-editing &lt;code&gt;package-lock.json&lt;/code&gt; and friends&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero dependencies. Nothing to configure. Node reading a JSON payload and occasionally&lt;br&gt;
saying no.&lt;/p&gt;

&lt;p&gt;Four of them turned out more interesting than I expected when I started writing them.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Reading a secret is worse than writing one
&lt;/h3&gt;

&lt;p&gt;My first instinct was to guard the &lt;em&gt;write&lt;/em&gt; — stop the key from landing in a file.&lt;/p&gt;

&lt;p&gt;Then I thought about it for another minute and realised I had it backwards.&lt;/p&gt;

&lt;p&gt;The write path has a code review in front of it. Someone, eventually, looks at that diff.&lt;br&gt;
The read path has &lt;em&gt;nothing&lt;/em&gt;. When an agent runs &lt;code&gt;cat .env&lt;/code&gt; to check which variables&lt;br&gt;
exist, it gets a completely reasonable answer to a completely reasonable question — and&lt;br&gt;
every value in that file is now sitting in a transcript. Transcripts get stored. Synced.&lt;br&gt;
Occasionally pasted into a bug report by someone being helpful.&lt;/p&gt;

&lt;p&gt;Nothing changed on disk. &lt;code&gt;git diff&lt;/code&gt; is empty. And your credentials have left the building.&lt;/p&gt;

&lt;p&gt;So the guard blocks the read and suggests this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"^[A-Z_]*="&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same question, answered, minus the part that ruins your week.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. "Already applied" is unknowable. "Already committed" isn't.
&lt;/h3&gt;

&lt;p&gt;I wanted a guard that stops you editing a migration a database has already run.&lt;/p&gt;

&lt;p&gt;Small problem: a hook has no idea what your production database has run. It's a Node&lt;br&gt;
script with a JSON blob. It cannot phone Postgres.&lt;/p&gt;

&lt;p&gt;But it can ask git one question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ls-files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--error-unmatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathspec&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ignore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is this file tracked? That's it. That's the whole heuristic — and it's a &lt;em&gt;good&lt;/em&gt; one,&lt;br&gt;
because once a migration is committed, something somewhere has almost certainly run it.&lt;/p&gt;

&lt;p&gt;The lovely side effect: the migration you're still drafting is untracked, so the guard is&lt;br&gt;
invisible while you're writing and immovable the moment you're not. The git index draws&lt;br&gt;
that line for free, and I didn't have to invent a single config option to get it.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The dangerous &lt;code&gt;git add&lt;/code&gt; is the one that looks harmless
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;git add .env&lt;/code&gt; is fine, honestly. It's &lt;em&gt;visible&lt;/em&gt;. It's right there in the scrollback,&lt;br&gt;
you'd catch it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git add -A&lt;/code&gt; in a repo where nobody remembered to gitignore &lt;code&gt;.env&lt;/code&gt; — that stages it&lt;br&gt;
silently alongside forty other files, and then the commit message says "add feature", and&lt;br&gt;
nobody looks, and it's on GitHub.&lt;/p&gt;

&lt;p&gt;So this guard doesn't pattern-match the command at all. It asks git what a blanket add&lt;br&gt;
would actually pick up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--porcelain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--untracked-files=all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the part I'm quietly pleased about: gitignored files never show up in that output.&lt;br&gt;
Which means on a correctly configured repo, this guard is completely, permanently silent.&lt;br&gt;
It only ever speaks to the repos that have the problem.&lt;/p&gt;

&lt;p&gt;A guard nobody notices is a guard nobody uninstalls. That property is worth more than the&lt;br&gt;
check.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. One guard just admits defeat
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kubectl delete pod api-7f9d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which cluster is that? I don't know. You don't know. The agent doesn't know. The hook&lt;br&gt;
&lt;em&gt;definitely&lt;/em&gt; doesn't know, because the answer lives in a config file that the payload&lt;br&gt;
never carries.&lt;/p&gt;

&lt;p&gt;Every other guard in this repo reads intent off the tool call. This one can't. So it does&lt;br&gt;
the only honest thing available: it refuses until you write &lt;code&gt;--context&lt;/code&gt; and make the&lt;br&gt;
command say out loud what it's about to change.&lt;/p&gt;

&lt;p&gt;It isn't blocking a mistake. It's blocking an &lt;em&gt;ambiguity&lt;/em&gt; — a command whose transcript&lt;br&gt;
won't record what it did. I think it might be the most useful one in the set, and it's&lt;br&gt;
the only one that works by admitting it can't see.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three rules I had to get right before I'd accept anyone else's guard
&lt;/h2&gt;

&lt;p&gt;If this repo works at all, most of the guards in it will eventually be written by&lt;br&gt;
strangers. Which changes the design problem completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A broken guard must never block a tool call.&lt;/strong&gt; Someone will ship a bug. If their bug&lt;br&gt;
takes down my &lt;code&gt;git push&lt;/code&gt;, this whole idea dies. So every guard runs in its own&lt;br&gt;
&lt;code&gt;try/catch&lt;/code&gt;, and a throw is treated as "no opinion" with a grumble on stderr.&lt;/p&gt;

&lt;p&gt;Yes — that means a crashing guard fails &lt;em&gt;open&lt;/em&gt;. For a security tool that sounds&lt;br&gt;
indefensible right up until you picture the alternative: one bad merge and nobody on&lt;br&gt;
earth can commit until it's reverted. The plugin gets deleted, and a deleted plugin&lt;br&gt;
guards nothing. Fail-open keeps it installed. Installed is the entire game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silence means allow.&lt;/strong&gt; The dispatcher only ever emits JSON to &lt;em&gt;deny&lt;/em&gt;. &lt;code&gt;permissionDecision&lt;/code&gt;&lt;br&gt;
will happily accept &lt;code&gt;"allow"&lt;/code&gt;, which would stomp on your own permission settings — and&lt;br&gt;
this plugin has no business doing that. It gets one vote. The vote is "no".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precision beats recall, and it isn't close.&lt;/strong&gt; One false positive on correct work and the&lt;br&gt;
plugin is gone by lunchtime.&lt;/p&gt;

&lt;p&gt;So every guard ships its near misses as executable examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;examples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git push --force origin main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git push --force-with-lease origin main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git push --force origin feature/x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--force-with-lease&lt;/code&gt; against &lt;code&gt;--force&lt;/code&gt;. &lt;code&gt;.env.example&lt;/code&gt; against &lt;code&gt;.env&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;docs/package-lock.md&lt;/code&gt; against &lt;code&gt;package-lock.json&lt;/code&gt;. That's where false positives live, so&lt;br&gt;
that's what you have to write down.&lt;/p&gt;

&lt;p&gt;And those examples &lt;em&gt;are&lt;/em&gt; the test suite. &lt;code&gt;npm test&lt;/code&gt; walks every guard and asserts both&lt;br&gt;
lists.&lt;/p&gt;

&lt;p&gt;That was the design decision I'm happiest with, and it took the longest to see. The&lt;br&gt;
obvious version of this repo has a &lt;code&gt;guards/&lt;/code&gt; folder and a &lt;code&gt;test/&lt;/code&gt; folder and contributors&lt;br&gt;
write both. Except they don't. Nobody writes the second folder. Ever.&lt;/p&gt;

&lt;p&gt;Folding the tests into the guard definition means a contribution is one file — and that&lt;br&gt;
file isn't valid until you've stated, in code, what it deliberately lets through.&lt;/p&gt;
&lt;h2&gt;
  
  
  A note on bash, since someone will ask
&lt;/h2&gt;

&lt;p&gt;These are Node, not shell.&lt;/p&gt;

&lt;p&gt;The shell versions are about a third the length and would depend on &lt;code&gt;jq&lt;/code&gt;. I wrote this on&lt;br&gt;
Windows. A meaningful chunk of the people who'd want it aren't sitting in a Unix shell,&lt;br&gt;
and a guardrail that only protects developers who already have good tooling is a fairly&lt;br&gt;
useless guardrail.&lt;/p&gt;

&lt;p&gt;Node ships with Claude Code. The dependency is already paid for.&lt;/p&gt;

&lt;p&gt;Incidentally the whole plugin has zero dependencies, so it has no lockfile — which is a&lt;br&gt;
genuinely funny property for a project that ships a lockfile guard.&lt;/p&gt;
&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;The unit of contribution is one file. Copy &lt;code&gt;guards/_template.js&lt;/code&gt;, change five things,&lt;br&gt;
open a PR. Ten minutes, tops.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-guard-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Short statement of the rule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prevents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The specific thing that goes wrong when nobody is watching.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// return { reason } to deny, or null to stay out of the way&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="cm"&gt;/* payloads that must deny */&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="cm"&gt;/* payloads that must pass */&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in &lt;code&gt;guards/&lt;/code&gt;. It's live. There's no registry to update — the dispatcher just&lt;br&gt;
reads the directory.&lt;/p&gt;

&lt;p&gt;One field decides whether it merges, and it's &lt;code&gt;prevents&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;"It's bad practice" is not a &lt;code&gt;prevents&lt;/code&gt;. If you can't finish the sentence &lt;em&gt;"the last time&lt;br&gt;
this happened, what broke was…"&lt;/em&gt;, you've got a style preference, and style preferences&lt;br&gt;
belong in your own &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which brings me to why I stopped at thirteen.&lt;/p&gt;

&lt;p&gt;I can see the shape of four more. &lt;code&gt;terraform apply&lt;/code&gt; with no plan file. &lt;code&gt;docker system&lt;br&gt;
prune -a&lt;/code&gt; on a box that's also your build cache. &lt;code&gt;chmod -R 777&lt;/code&gt; as a debugging step that&lt;br&gt;
somehow never gets reverted. An &lt;code&gt;ALTER TABLE&lt;/code&gt; that takes a lock on fifty million rows.&lt;/p&gt;

&lt;p&gt;I have opinions about all four. I have incidents behind none of them.&lt;/p&gt;

&lt;p&gt;That's the wrong ratio for writing a guard, because the &lt;code&gt;prevents&lt;/code&gt; field would be a&lt;br&gt;
guess — and a guess is exactly how you end up with a rule that fires on correct work and&lt;br&gt;
gets the whole thing uninstalled.&lt;/p&gt;

&lt;p&gt;Thirteen is where I ran out of scars. It is not where the list ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you've got the scar, write the guard.&lt;/strong&gt;&lt;br&gt;
→ &lt;a href="https://github.com/RabihJabr29/claude-guardrails" rel="noopener noreferrer"&gt;github.com/RabihJabr29/claude-guardrails&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>security</category>
      <category>node</category>
    </item>
  </channel>
</rss>
