manual mode, minus the nagging β deterministic guardrails that quiet the safe prompts in Claude Code and keep you in control.
A single PreToolUse hook + a curated allowlist that make Claude Code stop asking about safe work and start self-correcting its own bad habits β while still forcing a confirmation on the few things that genuinely deserve one.
It's a deliberate alternative to auto mode. Where auto mode asks an LLM classifier to judge each action (probabilistic, opaque, per-call), this takes the opposite bet: make the model's default behavior correct and frictionless, deterministically, and keep you in control of exactly what runs unattended.
What it is β and isn't. This corrects a cooperative model's habits (to cut prompts) and forces a confirmation on a few risky git ops Claude Code waves through. It is not a security boundary and does not defend against an adversarial or prompt-injected agent β a determined one can slip any string heuristic. It works alongside Claude Code's own allowlist and sandbox, not as a replacement for either: the sandbox is your wall against a hostile process; this is habit-correction for a well-behaved one.
Most Claude Code prompt fatigue isn't from dangerous commands β it's from safe ones dressed in shell habits: a grep piped into wc, a cmd 2>&1 | tail, a cd repo && git status, an npx jest instead of npm test. Each is harmless, none matches an allowlist, so each prompts. People get tired of approving them and flip on a blanket "just allow everything" mode β trading away all oversight to escape the noise.
This hook attacks the root instead. When the model reaches for one of those shapes, the hook blocks it with exit 2 and a one-line correction on stderr ("run it bare β the harness already returns stdout, stderr, and the exit code"). The model reads that, rewrites the command into the plain form, and retries. You never see a prompt, because the command that finally runs is the one your allowlist already trusts. The model also, over a session, stops producing the bad shapes at all.
A few principles fall out of that:
- Sanity > prompts > tokens. Pick the correct, reviewable command first; avoid a needless prompt second; save keystrokes/tokens last. The recurring failure is inverting it β a "clever" one-liner that saves a token but prompts and mutates blind.
- A block should mean "rewrite," not "bypass." So the escape hatch (below) isnever mentioned in a rule's message . When you're blocked, the only thing on screen is how to do it right β reaching for the override is a conscious act, not the reflex.
- Force the prompts that should exist. Claude Code read-only-misclassifies some git subcommands β
git add,git reset,git branch -Dβ so they auto-approve with no prompt. The guard overrides that and forces a confirmation. The index and your uncommitted work are yours. - Success is silent, so measure it. Every use of the escape hatch is appended to a log, so "no prompts" can't quietly hide overuse.
Self-corrects (exit 2 β the model rewrites, no prompt):
| Shape | Correction |
|---|---|
grep /find piped or in$(...) |
run the search bare, act on the result in a separate call ( β¦ | wc -l counts are exempt) |
cat , leadinghead /tail on a file |
use the Read tool |
cmd | tail ,cmd 2>&1 ,echo $? ,cmd > /tmp/β¦ |
run it bare β the harness returns full stdout/stderr + exit status |
for /while loop |
separate/parallel tool calls |
cd repo && git β¦ |
standalone cd , then plain git |
git -C <path> β¦ |
plain git (or standalone cd ) |
npx jest / raweslint when a script wraps it |
npm run <script> |
sed -i ,node -e β¦ writeFile |
the Edit/Write tool |
python /node parsing JSON |
jq |
Forces a confirmation (permissionDecision: ask) β the workflow gates:
The default rules for Claude Code auto-approve some git ops (most notably, git add.) The guard forces the confirmation you'd want. Same purpose as the corrections above (your workflow, made deterministic), pointed the other way: those remove an unwanted prompt, these add a wanted one.
- Mutating
git branch(-f/-d/-D/-m/-c), which CC auto-approves. - Any index/worktree/history mutation:
add reset restore rm stash checkout switch clean commit push.
The escape hatch: put #override anywhere in a command to skip the overreach rules (never the workflow gates). Use it for the rare genuinely-necessary pipeline. Every use is logged to ~/.claude/hooks/override-escapes.log.
- Copy the hook and make it executable:
mkdir -p ~/.claude/hooks
cp hooks/guard-bash-overreach.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/guard-bash-overreach.sh
- Wire it into Claude Code settings (
~/.claude/settings.jsonfor all projects, or a project's.claude/settings.local.json). Merge thehooksblock, the starterallowlist, and thedefaultModefromsettings.example.json. - Verify it's wired β feed the hook a command that trips a rule and watch it correct you (run this in your own terminal): You should see
jq -nc --arg c 'cat somefile' '{tool_input:{command:$c}}' | bash ~/.claude/hooks/guard-bash-overreach.sh
Overreach: 'cat' in a Bash call. Read files with the Read toolβ¦ on stderr. Silence means the hook didn't run β check the path.
The sample sets defaultMode: acceptEdits β auto-accept in-project file edits (Edit/Write) while every Bash command stays under the guard and allowlist. That's the sweet spot this is built for: manual control over what runs, no friction on what you write. Use default (manual) if you want edits to prompt too β but not auto/ bypassPermissions, which hand back the oversight this exists to keep.
The hook is re-read on every Bash call, so edits take effect immediately. The allowlist and defaultMode are read at session start β reload settings (or restart) after editing them.
The hook shrinks how big your allowlist needs to be (it corrects shapes instead of demanding an entry), but you still add the safe commands your project uses. The method:
- Don't pre-guess. When a safe command prompts, add it β that's the signal.
- Add read-only and idempotent commands , scoped tight:
Bash(npm run test),Bash(npm run build),Bash(git diff *). Keep mutations and network/arbitrary-code out (let them prompt). - Prefer the wrapped form. Allowlist
npm run <script>, not the raw tool β the script carries required flags, and Rule 10 steers you there anyway. - Wildcards cross spaces but not
/in Claude Code's matcher; a trailing:*is legacyprefix matching (which turns an earlier*literal). Pin host/port literals in URL rules (http://localhost:3000*, nothttp://localhost:*) so look-alike hosts don't match. - Skills can re-open doors. A skill's
allowed-toolsfrontmatter is anadditive grant β a blanket likeBash(some-cli:*)auto-approves every subcommand of that CLI while the skill runs, overriding your settings. Audit skill frontmatter, not just settings.
bash tests/run.sh feeds fake tool payloads to the hook and asserts each outcome (block / gate / allow) across every rule, with explicit regression cases for the tricky ones (quoted git subcommands, the #override marker appearing in data, combined sed flags). It runs under an isolated $HOME, needs no setup, and exits non-zero on any failure β CI-ready. Add a case whenever you add or change a rule.
Why this instead of the built-in fewer-permission-prompts skill?
They attack the same annoyance from opposite ends, and they compose β this isn't a replacement.
fewer-permission-prompts scans your transcripts and grows your allowlist to match the commands the model already runs. It's official, zero-setup, and a great way to seed a starter allowlist from real usage.
quiet-guardrails goes the other way: instead of widening the allowlist to fit the model's habits, it fixes the habits so a small allowlist suffices. When the model reaches for cmd 2>&1 | tail, cd repo && git status, or npx jest, the guard rewrites it to the plain form your allowlist already trusts. Three things follow:
- The allowlist grows to match mess; the guard removes the mess. An uncorrected model, over a week, runs
npx jest,jest --silent,npm test 2>&1 | tail,npm test -- --findRelatedTests x, andcd frontend && npm testβ five shapes for "run the tests," each its own allowlist decision, and some (the2>&1 | tail, the&&) things you shouldn't bless at all. Corrected, they collapse tonpm testandnpm test -- <args>β two entries.fewer-permission-promptsgrows the list to fit the variety; the guard shrinks the variety. - It adds friction, not just removes it.
fewer-permission-promptsis purely additive β it only ever makesmore things auto-approve. quiet-guardrails alsoforces a prompt ongit add/reset/branch -Dβ index and history mutations Claude Code silently auto-approves. A list-only tool structurally can't add that. - And the model actually learns. Because the correction landsat the command , the bad shapes taper off over the session β you're training the habit, not just filtering the output. An allowlist never does that; it only accumulates.
Use both: seed the allowlist with fewer-permission-prompts from real usage, then let quiet-guardrails correct habits and add the workflow gates. Each makes the other's job smaller.
Why a hook and not a rule, skill, or CLAUDE.md instruction?
Because those are advice the model can ignore β and reliably does, at exactly the wrong moment.
A rule or skill loads into context and asks the model to comply ("run searches bare, don't pipe into wc"). That holds right up until the model is heads-down mid-task and reaches for the shell habit anyway β the instruction is present, it just doesn't fire when the command is composed. A PreToolUse hook fires deterministically, on every command, remembered or not, and hands back a specific correction at the instant it's needed, so the model rewrites and retries.
Two things make it non-negotiable rather than nice-to-have:
- This tool exists because the advisory version didn't stick. The bad shapes kept recurring despite being written down; the
exit 2feedbackat the command is what actually changed behavior. You can't teach a habit with a doc the model skims once a session. - A guardrail has to be un-ignorable to be a guardrail. "Force a confirmation on
git add" as arule is worthless β the model can just not. As a hook returningpermissionDecision: ask, the harness enforces it. You can't build an enforced gate out of a suggestion.
This isn't "hooks beat rules" β rules and skills are the right tool for judgment a regex can't capture (taste, architecture, "is this proportionate?"). The split: deterministic, mechanical command shapes go in the hook; genuine judgment stays in your instructions.
Why not just use auto mode?
Auto mode hands each decision to an LLM classifier β probabilistic and per-call. quiet-guardrails keeps a deterministic allowlist you control and never auto-approves anything you didn't list; it cuts prompts by fixing the model's commands, not by trusting a judge to wave them through. The intro has the fuller contrast.
- It's opinionated and tuned to a specific setup (see the ENVIRONMENT ASSUMPTIONS block at the top of the hook): a native Claude Code build where
grepis ugrep and there are no Grep/Glob tools, and npm projects for thenpm runrule. On other builds some corrections flip (e.g. bare grep/find would steer to Grep/Glob tools). Read the rules before adopting; they're plain shell and easy to trim. - Calibrated to observed Claude Code behavior (developed against 2.1.x). The rules encode current CC quirks β which git subcommands it mis-classifies as read-only, how its allowlist matcher treats
*and:*. Anthropic can change these between versions: if they fix a misclassification a workflow gate just goes redundant (harmless), and the allowlist-matcher notes may need re-checking. Re-verify against your build. - The git workflow gates assume you drive staging/commits/pushes. If you want an agent to commit unattended, drop
commit/pushfrom Rule 8b. - Not a security boundary. It corrects acooperative model; a determined or prompt-injected one can defeat string heuristics, and it deliberatelyfails open . See**KNOWN-LIMITATIONS.md** for the specifics and why they're out of scope. If you need a real boundary, use the sandbox.
0BSD (SPDX: 0BSD; OSI-approved) β public-domain-equivalent, no attribution required. Take it and do whatever you want with it.
Built with Claude Code, and largely written by Claude β which is part of why it's 0BSD.