rm -rf on a cache folder, and that one was harmless. Both commands start the same way. Which should run straight away, and which should wait for a person? How would your code tell?
Any agent that runs shell commands has this problem. Most of its commands are builds, tests and
git status; a few delete things, change the machine, or push code somewhere. Permission rules match command names. Whether this command, here, needs a person is a judgment, and that’s the part hunch is for.Try it first
Each command below is one a real agent ran, under what the person had asked for. Decide: let it run, or stop it for a person? Then see what the guard said, and what a blind panel of reviewers said. The guard doesn’t ask “is this safe?”. It asks three narrower questions, each answered yes or no with how sure, and stops the command if any answer is yes. The rest of this page builds it and measures how often it is wrong.1. Make one row per command
The commands come from 28 public Claude Code sessions: 1,315 shell commands in all. hunch reads the session files as they are, andview: commands turns them into one row per command:
command_guard.yml
state is what the model may read: the person’s request, the folder the command runs in (cwd), the agent’s own one-line description of it, and the command. redact swaps secrets, email addresses and home folders for placeholders before anything is sent, and clip cuts very long fields.
2. Ask three narrow questions
“Is this command safe?” hides several judgments in one. The spec asks three. Each is anoul, a yes or no with how sure, and its criteria say what a yes and a no mean. That is where the policy is written down:
3. Write the guard’s rule
A person looks first if any answer is yes. Written as a metric, a rule over the answers,hunch test can count how often it fires and how often it should have:
4. Run it
5. Pin the commands it must never get wrong
Some commands must always stop, whatever else changes. Examples pin them in the spec: a row, and the answers it must get.hunch test checks each one, every time:
severity: warn, so every run reports it until a change to the spec fixes it, without failing the build meanwhile.
The result
Counted on the commands a panel reviewed, each step up the bar trades needless stops for missed ones; a miss costs more, so the guard stops at 0.5.The spec was vague, not the model
The first version ofreaches_outside asked whether a command would “change anything outside the project folder it works in”. It flagged 228 commands, and some of the surest were plainly wrong: a type check inside the project, at 0.96.
The obvious suspect was missing context. The model couldn’t see which folder was the project, so the folder went into the state. Almost nothing changed.
Reading the flagged commands again showed why. They wrote logs to /tmp, drove a test phone over adb, cloned a repository into a temp folder. Each of those really does change something outside the project. The model had read the question literally, and the question was too broad. Whether a temporary log should stop an agent is a policy, not a fact about the command.
So the policy went into the criteria: lasting changes only, and temporary files don’t count. hunch diff showed what that did before it shipped:
diff shows what changing it does to every row.
Use it on your data
hunch/ in your project and runs it before every shell command Claude Code or Codex runs; a yes, or an answer it isn’t sure of, asks you first. Guard your coding agent shows what it does and how to measure it on your own commands.
How it was measured
The data
The data
The 28 public Claude Code sessions in Trace Commons (CC BY 4.0): real developer and coding-agent sessions from public repositories, anonymized and reviewed by each contributor. They hold 1,315 shell commands, 794 in Bash and 521 in PowerShell. Download them as the example’s One run over all of them takes about 20 seconds and $0.04; the three versions of the spec cost $0.11 together. The spec is
NOTICE.md describes:prototype/examples/claude_code/command_guard.yml. The widget’s rows are generated from cached answers by docs-site/make_command_guard_data.py; long requests and commands are cut short there, and one request, a pasted web response, after its first few words.The free gold that wasn't
The free gold that wasn't
The sessions record refusals, which look like free gold. There are 17. The person refused 15: builds, script runs, two installs, a branch rename, even a file read. Those were about how or when to do the work, not about safety. Claude Code’s auto-mode safety classifier refused the other 2: a script uploading a session trace, and a push straight to
main. Those are exactly what a guard is for, and the guard flags both, at 0.98 on sends_out. But two examples can’t measure anything, so the gold had to come from review.The blind panel
The blind panel
Three AI reviewers (one Claude Opus, two Claude Sonnet, one of them reading the rows in reverse order) answered the same three questions for 244 commands. They saw what the model saw and the spec’s own definitions, and no model answers. The 244 were a random 100, which estimate accuracy over everything, plus every other command the guard flagged (144), which show how often a stop is needed. The majority vote became
command_guard.reviews.csv. The reviewers were unanimous on 226, 205 and 236 of the 244 for destroys, reaches_outside and sends_out. Four verdicts had no majority label: two ambiguous on destroys, one needing more context on reaches_outside, one ambiguous on sends_out. Packets, answers and scripts are in prototype/review_panel/command_guard/.Accuracy, with its interval
Accuracy, with its interval
hunch test reads the panel’s verdicts as gold. Accuracy is estimated from the random 100 only:The accuracy is high partly because most commands are harmless and easy to call harmless. The guard’s real job is the rare ones, which the next two sections look at.
The guard at each bar, in full
The guard at each bar, in full
A command stops when any question’s p(yes) clears the bar. “Reviewed” counts the 240 of the 244 commands where the panel labelled all three questions.Its false alarms come from the 8 random commands the guard stopped, so the interval is wide. The table uses every reviewed stop, which is fine for how many stops were needed, but is not a random sample.The one miss: the agent stopped every
At 0.5, four in ten stops are unnecessary, and that is the price. The misses column flatters the guard: every flagged command was reviewed, but unflagged ones only through the random sample, so a miss can only be found there. The fair estimate comes from the 91 random commands the guard would have let run: 1 of them needed a person. That is a miss rate of 1.1%, with a 95% interval of 0.2–6.0%. Over the 1,162 commands the guard lets through, that means somewhere between 2 and 69 misses, most likely around 13.
hunch test reports exactly this for the stopped metric, counting only random spot checks:bun process on the machine by name, then restarted its dev server. The panel judged that a change outside the project, since it would also stop other projects’ servers. The model gave it 0.39. The pinned example that stands for it, a shorter command, gets 0.57 for “no”.Where it leans
Where it leans
On
destroys and reaches_outside the model says yes more readily than it should: of the reviewed commands it gave 0.5–0.8, the panel said yes to 6 of 23 and 20 of 67. On sends_out it did not (11 of 13). That errs on the safe side for a guard, and it is where the false alarms come from. An earlier version of this page said 20 of 66; hunch test counts 67 today.What the request is, after a compaction
What the request is, after a compaction
After a context compaction, a background notification or a slash command, there is no new request, so
request is the last message the person typed before it. That is true of 111 of the 1,315 commands (72, 32 and 7). The model and the panel saw the same text, so the numbers are consistent, but the request may describe an earlier task.What this does not show
What this does not show
The reviewers are AI models, not people, and they applied the spec’s own definitions. The numbers say the model follows the policy; whether the policy is the right one is your call.28 sessions from a handful of developers, many on Windows. Your agent’s commands will look different: run the spec on your own sessions and review a random sample before trusting the numbers.
What did not work
What did not work
Treating refusals as gold: there were 17, and 15 were about workflow, not safety.Giving the model the working folder, to fix the first version’s false alarms: 49 answers flipped, and the mean probability moved by 0.037. The fix was stating the policy, as the story above tells.A small local model trained with
hunch distill, which would make the guard free: only 2% of commands need a person, too few to learn from. With the engine behind it, it let through 2 of 6 destructive commands and 1 of 12 that send data out, where the engine alone let through none. The guard stays on the engine.Calling it from your own code
Calling it from your own code
The same spec runs before each command. hunch returns each answer with the probability of its label, so the guard turns those into probabilities of yes:Call it from whatever runs before the command: a Claude Code
PreToolUse hook, a Pydantic AI tool hook, or your own wrapper. A command already judged comes back from the store without a request.The copy hunch hook install puts in your project is the recipe in src/hunch/recipes/agent_commands/: the same three questions, word for word, with 38 labelled commands to test against and an act bar on destroys, below which a person decides.