C2 · lesson 4 · 10 min read
Hooks
Run your own scripts at fixed points in Claude Code's lifecycle, for example to block a commit that contains secrets.
Հայերեն
Official video by Anthropic on YouTube. Pressing play loads it from youtube-nocookie.com. ·open on YouTube
Permission rules decide whether a tool may run. Hooks let you run your own code at fixed points, for example before a tool call, after a file edit, or when a session starts. Use them for checks that can’t be written as a simple pattern: secret scanning, formatting after edits, or notifying a channel.
Events you will use most
| Event | Fires | Typical use |
|---|---|---|
PreToolUse |
Before a tool call; can block it | Policy checks, secret scanning |
PostToolUse |
After a tool call succeeds | Run formatters or linters on edited files |
UserPromptSubmit |
When you send a prompt | Add context, reject prompts |
SessionStart |
When a session starts or resumes | Load environment info |
Stop / SubagentStop |
When Claude or a subagent finishes | Notify, or ask it to keep going |
PreCompact |
Before context is compacted | Save notes |
There are many more (for example PermissionRequest, Notification,
SessionEnd). The reference page has the full list.
Configuration shape
Hooks live in the same settings files as permissions:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git commit *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-secrets.sh",
"timeout": 30
}
]
}
]
}
}
matcherpicks the tool. It can be an exact name, a|list such asEdit|Write, or a regex;*or an empty matcher matches everything.ifnarrows the hook further with a permission-rule pattern.${CLAUDE_PROJECT_DIR}is the project root, so the path works from any subdirectory.typecan becommand,http,mcp_tool,promptoragent.
Input and exit codes
A command hook gets a JSON object on stdin. It includes session_id, cwd and
hook_event_name, and for tool events also tool_name and tool_input. The hook
answers through its exit code:
Exit code 2 is the one that blocks. The stderr text goes back to Claude as the
reason, so write something actionable. Instead of an exit code, a hook can exit 0
and print JSON. For PreToolUse that JSON is a
hookSpecificOutput with permissionDecision set to allow, deny, ask or
defer and a permissionDecisionReason.
Example: block secrets in commits
#!/usr/bin/env bash
# PreToolUse hook: refuse git commits whose staged diff looks like it has secrets.
set -euo pipefail
input=$(cat)
cmd=$(jq -r '.tool_input.command // ""' <<<"$input")
[[ "$cmd" == git\ commit* ]] || exit 0
cd "$(jq -r '.cwd' <<<"$input")"
if git diff --cached -U0 | grep -E -q 'AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----'; then
echo "Blocked: the staged diff contains what looks like an AWS key or private key. Remove it and use the secret store instead." >&2
exit 2
fi
exit 0
Recap
- Hooks run your code at lifecycle events.
PreToolUsecan block, andPostToolUsereacts to what happened. - Configure them in settings: an event, a matcher (plus an optional
if), and a handler. - Exit 0 continues, exit 2 blocks with stderr as the reason, and any other code is a non-blocking error.
- Treat hooks as code with your permissions, especially in CI.
See it in action
1/5 Claude decides to call a tool: here Bash running git commit.
all steps
- Claude decides to call a tool: here Bash running git commit.
- Before the call runs, every matching PreToolUse hook receives it as JSON on stdin (tool_name, tool_input, cwd…).
- Exit code 2 blocks the call. The hook's stderr goes back to Claude as the reason, so Claude can fix the problem.
- Exit 0 lets the call go ahead, still subject to the normal permission rules.
- After the tool succeeds, PostToolUse hooks run: a good place for formatters and linters.
transcript
> commit the new CI config
Bash(git commit -m "ci: add deploy job")
PreToolUse hook blocked the call (exit 2):
Blocked: the staged diff contains what looks like an AWS key or private key.
The staged ci.yml has an inline access key. I'll move it to a repository secret.
Edit(.github/workflows/ci.yml) → uses ${{ secrets.AWS_ROLE_ARN }}
Bash(git commit -m "ci: add deploy job") ✓$ ./try-it.sh
Try it: block commits that contain secrets
- Save the script from this lesson as
.claude/hooks/block-secrets.shand make it executable. - Register it under
PreToolUsewith matcherBashand"if": "Bash(git commit *)"in.claude/settings.json. - Stage a file containing
AKIAIOSFODNN7EXAMPLEand ask Claude to commit it. Confirm that the hook blocks the commit and that Claude sees your reason. - Replace the grep with
gitleaks protect --stagedif you have gitleaks installed.
Check yourself
4 questions. Original practice questions, not taken from the real exam.