Spec file
Claude Code hooks: checks before a tool call
Use PreToolUse, matchers and exit codes to build an explainable command check. Source-based; not tested.
A repeated check written only in a prompt still depends on the model choosing to run it. A command hook attaches that check to a specific lifecycle event. This page rejects one demonstration command to explain event input, matching and return values. It does not perform a publication or treat a simple string comparison as a complete shell command filter.
- Verified
- · Not tested by us
- Applies to
- Claude Code Based on Claude Code documentation read on 2026-11-03; behavior depends on the installed version and organization settings.[1]
- Tools
File locations
| Path | Scope | How it is read |
|---|---|---|
.claude/settings.json | Project hook registration, with scripts stored in .claude/hooks/ | Claude Code executes registered commands when the matching event occurs.[1] |
When to use
- You need a deterministic check to run on a lifecycle event rather than an open-ended model judgment.[1]
When not to use
- You expect PostToolUse to undo an external action or want to treat a string match as a complete shell parser.[1]
Choose before or after the action
PreToolUse runs before a tool call and can block it; PostToolUse handles work after a call. A matcher selects tool names such as Bash within that event. It does not infer your business condition. A command hook receives JSON on stdin, so its script must read fields belonging to the event it handles.[1]
Register a demonstration check
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "python3 .claude/hooks/demo-command-check.py"}]
}]
}
}If hooks already exist, add the event or handler without replacing other events. This example assumes that the command runs from the project root and Python 3 is available. Confirm the script path before saving it and inspect registration with /hooks.[1]
Read input and return a decision
import json
import sys
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "").strip()
if command == "npm publish":
print("Demo publication requires separate review.", file=sys.stderr)
sys.exit(2)
sys.exit(0)
The documentation identifies exit code 2 as a blocking error whose effect depends on the event. With PreToolUse it blocks this call. The script matches only an exact string; additional arguments, shell wrappers and other publication paths are outside its coverage. It demonstrates the return protocol rather than a complete permission policy.[1]
Check the script before connecting the event
- Feed simulated tool_input.command values to the script: npm publish should exit 2 and npm test should exit 0.
- Keep shell startup noise out of stdout; place error explanations on stderr and distinguish script failures from deliberate blocks.
- After integration, inspect /hooks and event logs. If it does not fire, check the event name, matcher, working directory and permissions.
The JSON parses, and the Python script was executed with the two simulated inputs. Claude Code and real hooks were not run, and npm publish was not executed. Validate the full event path in your own disposable project before relying on the hook.[1]
Sources
All links are official pages; verification dates are listed per source, and links point to the version checked.