My guard never ran, and nothing told me
I put a safety rule inside each agent's own file. In the setups I tested, it never fired. The same rule in the project settings did.
- Filed
- Reading
- 4 min
- Outcome
- Fixed
On 17 October I was building a team of agents for my ad and content work. Each agent had its own definition file, a plain text file that says what the agent is for and what it may do. I wanted a guard in each one: a small script that checks every command before it runs, and blocks the ones that change the ad account or push things out.
I wrote the guard into those files. It never fired.
Nothing showed an error on screen. The agents ran, the commands went through, and the guard sat there looking like it was working. I only found out because I tested it, with a harmless command that the guard should have blocked. It went through.
What a hook is
A hook is a script Claude Code runs just before a tool does something. If the script says no, the action stops. Hooks can live in several places. Two matter here. One is the top of an agent’s own file, in a header block called frontmatter. The other is the project’s settings file, .claude/settings.json, which is one shared file for the whole project.
I used the first place. If the agents had been in real use, nothing would have stopped them writing to the ad account or my task list.
Why it didn’t run
Claude Code has a trust prompt. When you open a folder for the first time, it asks whether you trust it. Hooks in an agent’s frontmatter run only if that exact folder passed the prompt. Trusting a parent folder doesn’t count. Running with -p (one command, no chat) or through the SDK (Anthropic’s toolkit for running agents from code) doesn’t count either. The docs say this is the cause. My agents ran through T3 Code (the desktop app I use to run Claude Code) and through claude -p, where the prompt never appears, and that fits what I saw.
The settings-file hook has no such condition. It ran in all of those cases, and it is told which agent is calling, in a field called agent_type. So one rule in the settings file can say “agents with this name prefix get checked, everyone else passes”.
I moved the guard there. On 17 October the first test blocked one agent in claude -p and a second in T3 Code. On 18 October I ran all 12 agents in fresh claude -p sessions, each with one harmless echo holding a blocked pattern. The 10 agents with no right to those actions were blocked. The 2 that each own one (ad changes, site deploys) passed.
The guard also fails closed. If its helper tool is missing, or it can’t read what it was given, it blocks. A guard that fails open is a guard that quietly turns off.
What happened to the team
On 18 October I stopped the whole team. They were drafts, not agents anyone could use yet. None had been running. The files were moved aside, reversibly. The guard stayed in the settings file, and it still applies to the main session’s task-list tools.
On 26 October I changed the task-list rule. Every change that isn’t a read now asks me first. The hook returns “ask” for those, and the settings file lists the write tools under ask.
The fix: make the agent prove each guard fires
Don’t trust that a guard exists. Make it fire. Paste this into the chat of the project you want to check.
Check that every safety guard in this project actually fires. Change nothing in any file.
1. Run `claude --version` and show me the output.
2. Read .claude/settings.json, .claude/settings.local.json and ~/.claude/settings.json. List every hook: its event, its matcher, and its script.
3. Read every file in .claude/agents/. List any that declare hooks in their frontmatter. Tell me whether each one will actually run, given that frontmatter hooks only run if this exact folder passed the trust prompt.
4. For each hook from step 2, trigger one harmless action that should be blocked. Use a command that only prints text, such as an echo that contains the blocked pattern. Never run the real action.
5. Show me the exact output of each attempt, including any block message.
6. Repeat step 4 once from a subagent, if this project has agents.
7. Finish with a table: guard, test you ran, BLOCKED or RAN, and the evidence. Any RAN is a failure. Say so in the first line.How to use it
Paste it into the chat of the project folder, not a different one, because hooks are read per project. Run it again after every Claude Code update, and after you add or move a hook.
It worked if the table shows BLOCKED for every guard, with a block message beside each one. If any row says RAN, that guard isn’t protecting you. Move it into .claude/settings.json and run the check again.
A settings-file guard has this shape. It is from the current hooks docs, which I read on 6 November. Exit code 2 is what blocks. onFailure: "block" makes it fail closed, and it needs Claude Code 2.1.295 or later. Put your own script path in.
This block does nothing useful on its own. It runs a script you have to write yourself, at .claude/hooks/guard.sh, and the script must be executable (chmod +x). If the script is missing, onFailure: "block" blocks every shell command, so create and test the script first. You can ask your agent to write it, then run the check above.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh",
"onFailure": "block"
}
]
}
]
}
}
Revision record
| First published. |