Build1 distinct publisher3 min readPublished
Allow and deny globs in settings.json cannot separate git status from git push --force, so the real enforcement moves into a PreToolUse hook that reads the command text, exits 2, and tells the model why it was refused.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
The glob layer cannot carry this alone because it matches the front of the command string. `Bash(git status:*)` admits arguments to that one subcommand; `Bash(git:*)` admits the whole of git, force push included [2]. Expressing "git, except one flag" means enumerating the subcommands you want, and that list goes stale the first time someone needs `git stash`. The published allow list takes the narrow route: `ls`, `git status`, `git diff`, `Read(*)`, `Grep(*)` [3].
The hook is where a predicate becomes possible. Claude Code runs it before every tool call, hands it the event on stdin, and treats exit 2 as a denial, passing the reason string back to the model [4][5]. That last part is the good engineering here. A refusal that arrives in band, as text the model reads, is repairable: the agent is told that force push was blocked because it overwrites history [7] instead of watching a call disappear. It is also the one place where a guard doubles as a hint system, so expect a determined agent to try another spelling of the same intent.
Notice what the script checks first. If `tool_name` is not `Bash`, it returns 0 [5]. Write, Edit and every MCP call pass uninspected [14]. That is why the SELECT check has to live inside the MCP server, where the sample read-only SQLite tool refuses any statement that does not begin with `select` [12]. One shell deny list cannot cover an agent's reach, because most of that reach is not a shell.
Read the deny list the way you would read someone else's benchmark table. It names twelve categories [15]: recursive force delete on POSIX and PowerShell, force push, hard reset, pipe-to-shell from curl or wget, plus fork bombs, raw disk writes, sudo, format, shred, `del /s` and `rmdir /s` [7]. For that to transfer, your agent's worst available action has to be a shell verb this author thought of. `terraform apply` and `kubectl delete` are not in the set [7]. `sudo` is in it twice, once as a glob and once as a regex [16], which is the right kind of redundancy, because the two layers fail differently.
The patterns match literal text in `tool_input.command` [5][6]. A command assembled at runtime from variables, or reached through an alias, does not contain the tokens the regex is looking for [17]. Which is where the mode switch earns its keep: Mbouyap recommends a deny list for interactive work on your own machine and a fail-closed allow list for unattended runs or client work, logging every decision either way [11]. I read that as the actual finding. Pattern matching filters the commands you can name, and an unattended run should not be executing commands nobody named.
Two adoption costs, both small and both real. Every Bash call starts a Python interpreter, because the hook is wired as `python .claude/hooks/guard.py` [9][18]. And the write-up specifies exit 2 as deny without stating what any other nonzero code means [19], so if your CI image lacks that interpreter, you want to know whether the guard fails open before a live run tells you. The test he shows, piping a JSON blob in so that `rm -rf /` blocks and `git status` passes [10], is cheap enough to keep in CI as a unit test of the guard itself.
Ranked by verification strength, evidence, and original report placement.
Harry Philippe Mbouyap describes a three-layer approach to giving Claude Code unattended autonomy; all the code is MIT licensed and published in a repo at github.com/hbouyap/claude-code-safe-automation.
Permission globs are coarse: Bash(git:*) allows both git status and git push --force, so commands that matter need logic rather than a glob.
The example .claude/settings.json permissions allow Bash(ls:*), Bash(git status:*), Bash(git diff:*), Read(*) and Grep(*), and deny Bash(sudo:*), Read(.env) and Read(**/secrets/**).
The guard script loads the tool event as JSON from stdin, returns 0 immediately if event tool_name is not "Bash", otherwise regex-searches event tool_input command against a dangerous-pattern list and returns 2 on a match.
The rm pattern uses two lookaheads to require both a recursive flag and a force flag in any order, matched case-insensitively, so rm -rf, rm -R -f, rm --recursive --force and rm -Rf all match while plain rm file.txt passes.
The deny list shown covers recursive force delete on POSIX, remove-item -rec -for on Windows, git push with --force or -f ("force push can overwrite history"), git reset --hard, curl or wget piped to sh or bash, plus fork bombs, raw disk writes, sudo, format, shred, del /s and rmdir /s.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · September 2, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
The overnight batch does not die from a bad model, it dies when an MCP server stops answering1 distinct publisher
build
A subshell in the log line zeroed $? before the Claude Code hook could return it1 distinct publisher
build
Your agent supplies its own confirmation: confirm=True is not a permission boundary1 distinct publisher
build
A memory note is not a gate: blocking credential reads with a PreToolUse hook1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
One author, but code you can read
A good share of this story checks itself: the settings.json blocks, the guard source, the lookahead regex and the two test invocations are printed, so a reader can confirm what the code does without leaving the page. What can't be checked is the sentence the story hangs on. The only test pipes JSON into guard.py and reads that script's own exit status — Claude Code never appears in it, and no vendor documentation is cited for exit 2 meaning deny. The deny list has the same shape of gap: five patterns are shown, twelve categories are named, and the other seven live in a comment.
A repo and a licence, nothing behind them
There is a public MIT repository and that is the whole of it: no stars, forks or installs quoted, no team saying this runs their unattended agents, not even the author describing a workflow he shipped with it. A tutorial this fresh wouldn't have traction to show yet, so we record the publication and decline to score uptake from it.
'Structurally blocked' outruns what regexes can promise
The write-up hedges honestly in places — permissions are 'necessary but not sufficient', a Windows-blind guard is 'theater on half your machines'. The overreach sits in the closing promise that the handful of harmful actions are structurally blocked, so you can point the agent at write-test-deploy and walk away. A regex over the literal command string doesn't see a path built from variables or an alias, never runs for Write, Edit or MCP calls, and the piece defines no outcome for a guard that fails to start. That is a good speed bump described as a wall.
Free repo in front, paid kit behind
The post ends where these posts tend to: a fuller commercial 'MCP & Guardrails Kit' with the fail-closed allow-list mode, three server templates and an installer, plus setup work for teams. Note which side of that line things fall on — the free code blocks known-dangerous commands, while the stricter posture the author says you need for unattended and client work is in the product. The disclosure is right there in the open, which is better than the alternative, but it explains the shape of what's given away.
Sure about the code, unsure about the contract
We can be fairly firm on what the guard does, because it is on the page and internally consistent, and firm on the derived limits, because they follow from that same code. Confidence drops on how Claude Code treats a hook's exit status, and drops to nothing on whether anyone runs this: one publisher, no vendor reference, no user reports.