Guides ·
4 Claude Code hooks examples — permission alerts, blocking risky commands, logging, and a browser view
How to add Claude Code hooks to settings.json, with 4 examples you can copy. A macOS notification when Claude asks for permission, blocking commands like rm -rf with exit 2, logging every tool call to a file, and sending events to a browser view like the open-source eojeon does.
Claude Code hooks run your own command at set moments, such as before and after a tool call, on a permission request, or when Claude finishes a reply. You add them under "hooks" in ~/.claude/settings.json or a project's .claude/settings.json, keyed by event name. The command receives JSON on stdin (session_id, cwd, hook_event_name, tool_name, tool_input and more); exit 0 passes, exit 2 blocks (on events like PreToolUse), and any other code only logs a warning.
Claude Code: A Hands-On Guide · Part 3 of 10
A hook is a setting that says "when Claude Code does X, run this command." A few lines in a settings file let you get a notification when Claude asks for permission, block risky commands before they run, and keep a log of every action. This guide shows where hooks go, what they receive and return, and 4 examples you can copy. The examples are the approach hiio actually used while building the open-source eojeon. It was written by AI (Claude), and the facts are checked against the official Claude Code hooks docs.
Where hooks go
| File | Applies to |
|---|---|
~/.claude/settings.json |
All your projects |
.claude/settings.json |
This project (shared with your team) |
.claude/settings.local.json |
This project, only you (not shared) |
The structure is always the same: hooks → event name → array of groups (matcher + hooks) → command.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/guard.sh", "timeout": 5 }]
}
]
}
}
matcherfilters by tool name. Write it like"Bash"or"Edit|Write". Leave it out, or use"*", to match everything. Some events, likeUserPromptSubmitandStop, ignore the matcher.timeoutis in seconds. The default is 600 seconds, which is long, so set short hooks to about 5 seconds.- After adding hooks, type
/hooksin Claude Code to see which hooks are active and where each one comes from.
What a hook receives and returns
The command gets one JSON object on stdin. The common fields are session_id, transcript_path, cwd, permission_mode and hook_event_name. Tool events add tool_name, tool_input and tool_use_id. Events from a subagent add agent_id and agent_type.
| Exit code | Meaning |
|---|---|
exit 0 |
Pass. If stdout is a single JSON object, it is parsed. Otherwise it is treated as plain text |
exit 2 |
Block. On events like PreToolUse and UserPromptSubmit, the action stops and stderr is sent to Claude as the reason |
Anything else (including exit 1) |
Does not block. A warning is logged and the action continues |
exit 1 does not block. A blocking hook must end with exit 2.
Example 1. Get a macOS notification when Claude asks for permission
If Claude is waiting on "Can I do this?" while you are in another window, you may not notice for a long time. Attach a notification to PermissionRequest.
{
"hooks": {
"PermissionRequest": [
{ "hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Permission requested\" with title \"Claude Code\"'", "timeout": 5 }] }
]
}
}
This is for macOS. On Linux, swap in notify-send. On Windows, use a PowerShell notification command.
Example 2. Block risky commands before they run
Use PreToolUse with matcher: "Bash" to inspect the command first, and block it with exit 2 if it matches. You need jq (brew install jq).
#!/bin/sh
# ~/.claude/hooks/guard.sh (run chmod +x on it)
cmd=$(jq -r '.tool_input.command // ""')
case "$cmd" in
*"rm -rf /"*|*"git push --force"*|*"git push -f"*)
echo "Blocked: $cmd — check it and run it yourself" >&2
exit 2 ;;
esac
exit 0
The settings are exactly the example under "Where hooks go" above. When a command is blocked, Claude receives the stderr line as the reason and looks for another way.
Example 3. Log every tool call to a file
A log of what ran and when lets you answer "what command did it run earlier?" right away.
{
"hooks": {
"PreToolUse": [
{ "hooks": [{ "type": "command", "command": "jq -c '{t: now|todate, s: .session_id[0:8], tool: .tool_name, in: (.tool_input.command // .tool_input.file_path // \"\")}' >> ~/.claude/tool-log.jsonl", "timeout": 5 }] }
]
}
}
Careful: commands and file paths can contain secrets (tokens, passwords). Keep the log file readable only by you, and never upload it anywhere.
Example 4. Send events to a browser view (the eojeon approach)
This is how eojeon uses hooks:
- It launches Claude Code with
claude --settings <temporary settings file>. Your own~/.claude/settings.jsonis untouched, and the hooks apply only to that run. - The temporary file attaches the same hook command to 13 events:
SessionStart,UserPromptSubmit,PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied,SubagentStart,SubagentStop,Notification,Stop,StopFailureandSessionEnd. - The hook script shortens the JSON it receives (tool name, first line of the command, relative path, and secret-looking strings replaced with
***). Then it sends it only to a local server on127.0.0.1. It finishes within 1.5 seconds and always exits 0, so it never blocks Claude Code. - The browser draws a palace scene from these events. A subagent starting shows a minister entering the court. A permission request shows a minister asking the king for approval.
To see it, run npx eojeon-cli demo (it replays the scenes without Claude). To use it for real, run npx eojeon-cli. The code is in server/hook.js and server/sanitize.js on GitHub.
FAQ
Do Claude Code hooks slow down Claude Code?
A hook runs at its moment, and Claude Code waits until it finishes (or hits its timeout). The default timeout is 600 seconds, so for short jobs like notifications or logging, set something like "timeout": 5. eojeon's hook finishes on its own within 1.5 seconds.
Why doesn't my hook block the command when it exits with code 1?
exit 1 is a non-blocking error. The action continues and only a warning is logged. To block, end with exit 2 and write the reason to stderr.
Can I try hooks without editing my settings.json?
Yes. Put only the hooks in a separate JSON file and run claude --settings path/to/that-file. They apply only to that run. eojeon works this way. The official docs also show an example that turns off all hooks for one run (claude --settings '{"disableAllHooks": true}').
How do I see which hooks are active?
Type /hooks inside Claude Code. It shows each hook and which file it came from (user, project, local or plugin).
Sources
Written with AI (Claude) and checked against the linked sources.