Explainer · 347 seconds
Hooks: stop a dangerous command before it runs
A PreToolUse command hook can refuse matching tool calls before they run. A blocking example, its limits, and how blocking differs from asking for human approval.
Transcript
- Stop a dangerous command before it runs.
- This illustrative cleanup deletes a file you never committed.
- A hook can block it. Exit two refuses the call.
- But if your hook crashes, the command can still run.
- Here's why, and how to test it.
- A hook sounds like one more step in the build.
- But this one runs before a matched tool call. That timing matters.
- It can refuse the call before execution.
- A PreToolUse command hook runs your program before a matching tool call, and reads its decision.
- Before the call. That's where this check belongs.
- Here's the harness. The repository underneath. The project file, the skills, the hook.
- The project file and the skills give instructions. The model can still misread them.
- The hook can enforce a refusal. Permissions and a sandbox provide other controls.
- That doesn't make the hook infallible. Its matching and failure handling still have to work.
- So we need to check what it blocks, and what happens when it breaks.
- Here's a concrete version, with the shell already permitted.
- You ask it to clean up the repo before a push. It plans to remove build output and untracked files.
- It runs git clean, dash f d x. That also removes a file you never committed.
- Git has no copy of an untracked file. The hook belongs here, before the command runs.
- Before a tool call matched by your configuration, the hook program runs.
- It gets a JSON object on standard input: which tool, and what arguments.
- The program reads it, decides, and returns a result.
- For an exit-code-only command hook, the code matters.
- First, the blocking path: exit two.
- Exit two blocks the call. Whatever you wrote to standard error comes back as the reason.
- Exit zero with no decision leaves the normal permission flow in charge. That may still ask you.
- Other exit codes can be non-blocking errors. Without failure blocking, normal permissions still decide.
- So an unexpected script error can leave your check out of the decision.
- A hook error notice is not a refusal. Check failure behavior in the version you run.
- Use two to refuse. Test failures too.
- The other half of this is knowing what to put behind it.
- Keep prompts useful. Start with actions whose consequences are hard to reverse.
- Git helps only when the work you need is saved in a verified checkpoint. Uncommitted work can still be lost.
- Prompt for every routine step and you train yourself to click through. Save attention for consequential changes.
- Here are five categories to assess. These are policy goals, not coverage our little example proves.
- Changes you can't easily undo.
- Anything recursive and deleting. Anything that rewrites shared history.
- Changes to a non-disposable database. Anything that deploys, publishes or sends.
- And actions that spend money. The sample doesn't detect all of these categories.
- Now look at the configuration. This wires up a script; it is not the whole safeguard.
- In the project's settings file, under hooks, PreToolUse.
- The Bash matcher covers shell calls, including shell reads. It doesn't cover every other tool.
- The command points to a separate script. The book shows a small shell example.
- It checks literal command patterns. A match is refused. Plenty of other forms are missed.
- Now follow a matching command through an illustrative refusal.
- The agent proposes it. The hook matches it. Exit two blocks that call.
- Show this as blocked. There is no human approval prompt in this example.
- A refusal ends that tool call. It doesn't sit waiting for a yes.
- Actual approval needs a different result: a structured permission decision of ask.
- In an interactive session that requests confirmation. In a non-interactive run, an unanswered prompt can be denied.
- For our blocking example, the useful part is the reason returned to the agent.
- For example: blocked, git clean removes untracked files. Commit or stash first, then try again.
- So, what to do this afternoon.
- Keep the check small enough to review. Raw command prefixes do not make a safe allow list.
- Test malformed input, missing dependencies and timeouts with harmless fixtures. The book example still needs failure handling.
- Test the actual commands you use. Add alternate spellings and shell chains. Do not mistake a pattern list for a sandbox.
- And scope credentials and sandbox access. A local pattern check alone can't protect production.
- That's the useful distinction: blocking a match is not the same as asking a person.
- Before the call. Explicit refusal. Tested failures and scoped access.
- Stop a dangerous command before it runs.
- What would your hook block first?
Comments
No comments yet.