Coding Agents
Claude Code hooks builder
Build the hooks block for Claude Code’s settings.json from tested examples, with the Bash or PowerShell script each one runs, and check hooks you already have. It’s free and runs in your browser.
Save as .claude/settings.json. If the file already exists, paste it below to merge.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh",
"timeout": 30,
"onFailure": "block"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh",
"timeout": 30,
"onFailure": "block"
}
]
}
]
}
}
Save as .claude/hooks/block-rm.sh
#!/bin/bash
# block-rm.sh: Claude Code PreToolUse hook for the Bash and PowerShell tools.
# Blocks recursive deletes: rm -r, rm -rf, rm --recursive, Remove-Item -Recurse
# (and its aliases), rmdir /s. Exit 2 blocks the call; Claude reads stderr.
# Fails closed: if the input can't be read, the command is blocked.
block() {
echo "Blocked by the block-rm hook: $1" >&2
exit 2
}
command -v jq >/dev/null 2>&1 || block "jq is not installed, so the command couldn't be checked. Install jq: https://jqlang.org/download/"
input=$(cat)
cmd=$(jq -r '.tool_input.command // empty' <<<"$input" 2>/dev/null) || block "the hook input wasn't valid JSON."
[ -n "$cmd" ] || exit 0
cmd=${cmd//[\"\'\\^]/} # drop quotes, backslashes and carets, so "rm", r''m and \rm count as rm
shopt -s nocasematch
nl=$'\n'
rest="([[:space:]][^;&|$nl]*)?[[:space:]]" # more words of the same command
patterns=(
"(^|[^[:alnum:]_.-])rm$rest(-[[:alpha:]]*r[[:alpha:]]*|--r[[:alpha:]-]*)([^[:alnum:]-]|$)"
"(^|[^[:alnum:]_-])(remove-item|ri|rd|rmdir|del|erase)$rest-r(e|ec|ecu|ecur|ecurs|ecurse)?([^[:alnum:]-]|$)"
"(^|[^[:alnum:]_-])(rmdir|rd|del|erase)$rest/s([^[:alnum:]]|$)"
)
for re in "${patterns[@]}"; do
if [[ $cmd =~ $re ]]; then
block "recursive deletes aren't allowed (found: ${BASH_REMATCH[0]}). Delete specific files, or ask the user to run it."
fi
done
exit 0
Save as .claude/hooks/protect-files.sh
#!/bin/bash
# protect-files.sh: Claude Code PreToolUse hook for the Edit and Write tools.
# Blocks edits to protected files. Exit 2 blocks the edit; Claude reads stderr.
# Fails closed: no jq, or input it can't read, also blocks.
# File names (glob patterns), directories, and names that stay editable.
protected_names=('.env' '.env.*' '*.pem' '*.key')
protected_dirs=('.git' 'secrets')
allowed_names=('.env.example')
block() {
echo "Blocked by the protect-files hook: $1" >&2
exit 2
}
command -v jq >/dev/null 2>&1 || block "jq is not installed, so the edit couldn't be checked. Install jq: https://jqlang.org/download/"
input=$(cat)
file=$(jq -r '.tool_input.file_path // empty' <<<"$input" 2>/dev/null) || block "the hook input wasn't valid JSON."
[ -n "$file" ] || block "the hook input had no file path."
# Compare the path as Windows resolves it: forward slashes, no trailing dots or
# spaces on a name (.env. and ".env " are .env) and no :stream suffix.
check=$(sed -E 's#\\#/#g; s#:[^/]*$##; s#[. ]+(/|$)#\1#g' <<<"/$file") && [ -n "$check" ] || block "the file path couldn't be checked."
name=${check##*/}
shopt -s nocasematch # .ENV and .env are the same file on Windows and macOS
for d in "${protected_dirs[@]}"; do
[[ $check == */"$d"/* ]] && block "$file is inside $d/, which is protected. Ask the user to change it by hand."
done
for n in "${allowed_names[@]}"; do
[[ $name == $n ]] && exit 0
done
for n in "${protected_names[@]}"; do
[[ $name == $n ]] && block "$file is protected ($n). Ask the user to change it by hand."
done
exit 0
- The Bash scripts need jq: brew install jq (macOS), sudo apt-get install jq (Debian, Ubuntu) or winget install jqlang.jq (Windows). Without it, each guard blocks instead of running unchecked.
- Make the scripts executable on macOS and Linux: chmod +x .claude/hooks/*.sh. Git Bash on Windows runs them as they are.
- block-rm checks the full command text, so it also catches rm inside &&, pipes, $( ) and sudo. It also blocks git rm -r; edit the pattern if you need that.
- block-rm is a pattern check, not a sandbox. It misses other ways to delete a folder, such as find -delete, git clean, a command built from variables or a script Claude writes. For a hard rule, add permission deny rules too, such as Bash(rm -rf *).
- protect-files only sees the Edit and Write tools, and it checks the path Claude gives, not where a symlink points. A shell command such as echo KEY=1 >> .env gets past it; a permission deny rule such as Edit(.env) also covers shell redirects like that, and only the sandbox stops every process.
- To stop Claude reading .env as well, add a permission deny rule such as Read(./.env).
- Run /hooks in Claude Code to check they loaded. Hooks run with your user permissions: read every script before you save it.
Everything is generated in your browser; nothing you type or paste is sent anywhere.
Check and merge your existing hooks
Paste a settings.json, a hooks object or a plugin’s hooks/hooks.json. It’s checked against the hooks reference: event names, matchers, handler fields, which events support each handler type, and common Windows and quoting mistakes.
Steps
How to use the Claude Code hooks builder
- Pick the hooks you want, or add a custom one, and edit any options such as the protected file names.
- Choose Bash or PowerShell for the scripts and which settings file the hooks go in.
- If the settings file already exists, paste it into the checker so the new hooks are merged in.
- Save each file where its tab says, run chmod +x on the Bash scripts, and run /hooks in Claude Code to confirm they loaded.
Method
How it works
A Claude Code hook is a command, HTTP call, MCP tool call, prompt or subagent that Claude Code runs itself at a fixed point in a session, such as just before a tool call or when Claude finishes responding. Instructions in CLAUDE.md are advice the model usually follows; a hook runs every time its event fires, whatever the model decides. That makes hooks the place for rules that must hold, like never editing .env, and chores that should never be forgotten, like formatting. For a walk through every event with worked examples, read the Claude Code hooks guide.
Event, matcher, handler
Every hook has three parts. The event is the moment: PreToolUse before a tool runs, PostToolUse after it succeeds, UserPromptSubmit, Stop, SessionStart and 28 more. The matcher narrows it: on tool events it’s the tool name, so Edit|Write means either tool. A matcher made only of letters, digits, _, -, spaces, commas and | is compared exactly and is case-sensitive; anything else is an unanchored JavaScript regular expression, so Edit.* also matches NotebookEdit. Ten events ignore the matcher entirely. The handler is what runs. On tool events an if field can narrow one handler further with permission-rule syntax such as Bash(git push *), but on any other event a handler with if never runs.
How a hook answers
A command hook gets the event as JSON on stdin. Exit 0 means success, and Claude Code reads any JSON object printed on stdout. Exit 2 is a blocking error: on events that can block, the action stops and stderr is the reason (on PreToolUse Claude reads it and changes course). Any other exit code, including the usual Unix failure code 1, is a non-blocking error and the action goes ahead. For finer control a PreToolUse hook prints hookSpecificOutput.permissionDecision as allow, deny or ask, while PostToolUse, Stop and others use a top-level "decision": "block".
Why the guards fail closed
The quiet danger with hooks is that failure lets things through. A mistyped script path, a missing jq, a crash or a timeout is a non-blocking error, so a guard that breaks stops guarding without anyone noticing. Each guard here checks for its tools and its input and blocks if either is missing, and the option above adds "onFailure": "block" (Claude Code 2.1.295 or later) so that a script that can’t even start blocks too. Guards are still pattern checks, not a sandbox: for hard limits, pair them with permission deny rules, which a hook’s allow can never override; the settings.json and permissions builder writes those.
How it was checked
Every event, matcher rule, handler field, default timeout and exit-code effect comes from Claude Code’s hooks reference and hooks guide, read on 2026-10-13, when the newest release was 2.1.296. Each example’s script was run, exactly as its settings entry starts it, against sample hook input in Git Bash and Windows PowerShell 5.1, covering commands that must be blocked (including quoted and abbreviated forms) and ones that must pass, Windows and Unix paths, non-English characters, missing jq, unreadable input and paths that climb out of the project. The Bash guards are also run in the site’s unit tests, and every generated settings block passes this page’s own checker. Nothing was run inside a live Claude Code session, so behaviour there is as documented.
Examples
Worked examples
Block recursive deletes and notify when Claude stops
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh",
"timeout": 30,
"onFailure": "block"
}
]
}
],
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "printf '%s\\n' '{\"terminalSequence\":\"\\u001b]9;Claude Code needs your approval\\u0007\"}'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "printf '%s\\n' '{\"terminalSequence\":\"\\u001b]9;Claude Code finished\\u0007\"}'"
}
]
}
]
}
}
The first hook runs .claude/hooks/block-rm.sh before every shell command; the other two print a terminal notification that Claude Code shows for you.
Every hook event at a glance
| Event | Matcher | Exit code 2 | Handlers |
|---|---|---|---|
| SessionStart | how the session started | Shows stderr to you only; the session goes on | command, mcp_tool |
| Setup | the CLI flag | Ignored | command |
| UserPromptSubmit | None | Blocks the prompt; stderr is shown to you, not to Claude | command, http, mcp_tool, prompt, agent |
| UserPromptExpansion | command name | Blocks the expansion; stderr is shown to you | command, http, mcp_tool, prompt, agent |
| PreToolUse | tool name | Blocks the tool call; Claude reads stderr as the reason | command, http, mcp_tool, prompt, agent |
| PermissionRequest | tool name | Not honoured; the permission flow goes on unchanged. Deny with the decision object instead | command, http, mcp_tool, prompt |
| PermissionDenied | tool name | Ignored; the denial already happened | command, http, mcp_tool, prompt, agent |
| PostToolUse | tool name | Shows stderr to Claude; the tool already ran | command, http, mcp_tool, prompt, agent |
| PostToolUseFailure | tool name | Shows stderr to Claude; the tool already failed | command, http, mcp_tool, prompt, agent |
| PostToolBatch | None | Stops the agentic loop before the next model call | command, http, mcp_tool, prompt, agent |
| Notification | notification type | Ignored | command, http, mcp_tool |
| MessageDisplay | None | The original text is displayed | command, http, mcp_tool |
| SubagentStart | agent type | Shows stderr to you only | command, http, mcp_tool |
| SubagentStop | agent type | Keeps the subagent working, with stderr as its next instruction | command, http, mcp_tool, prompt, agent |
| TaskCreated | None | Cancels the task; Claude gets stderr as the tool error | command, http, mcp_tool, prompt, agent |
| TaskCompleted | None | The task stays open; stderr goes to Claude | command, http, mcp_tool, prompt, agent |
| Stop | None | Claude keeps working, with stderr as the reason | command, http, mcp_tool, prompt, agent |
| StopFailure | error type | Ignored | command, http, mcp_tool |
| TeammateIdle | None | The teammate keeps working, with stderr as feedback | command, http, mcp_tool, prompt, agent |
| InstructionsLoaded | load reason | Ignored | command, http, mcp_tool |
| ConfigChange | configuration source | The change doesn’t apply to the session (policy_settings always applies) | command, http, mcp_tool |
| CwdChanged | None | Shows stderr to you only | command, http, mcp_tool |
| DirectoryAdded | how it was added | Stderr goes to the debug log | command, http, mcp_tool |
| FileChanged | file names to watch, split on | | Shows stderr to you only | command, http, mcp_tool |
| WorktreeCreate | None | Any non-zero exit fails the creation | command, http, mcp_tool |
| WorktreeRemove | None | Any non-zero exit fails the removal if the directory still exists | command, http, mcp_tool |
| PreCompact | what triggered compaction | Blocks compaction | command, http, mcp_tool |
| PostCompact | what triggered compaction | Shows stderr to you only | command, http, mcp_tool |
| PreModelSwitch | model switched to | Blocks the switch and shows stderr to you | command, http, mcp_tool |
| PostModelSwitch | model switched to | Shows stderr to you only | command, http, mcp_tool |
| Elicitation | MCP server name | Declines the request; no dialog appears | command, http, mcp_tool |
| ElicitationResult | MCP server name | Blocks the response (it becomes a decline) | command, http, mcp_tool |
| SessionEnd | why it ended | Shows stderr to you only | command, http, mcp_tool |
From the hooks reference, checked 2026-10-13. Default timeouts: 600 seconds for command, http and mcp_tool (30 on UserPromptSubmit and model switches, 10 on MessageDisplay), 30 for prompt, 60 for agent; SessionEnd hooks share a 1.5-second budget.
FAQ
Frequently asked questions
Where do Claude Code hooks go?
Under a hooks key in a settings file: ~/.claude/settings.json for all your projects, .claude/settings.json to share them with your team through the repository, or .claude/settings.local.json for yourself in one project. Hooks from every file merge rather than replace each other. Run /hooks in Claude Code to see which loaded and where each came from.
How do I block a command with a hook?
Use a PreToolUse hook with the matcher Bash|PowerShell. The script reads the command from tool_input.command on stdin; to block it, it writes a reason to stderr and exits with code 2, and Claude reads the reason. Exit code 1 doesn’t block. The block-rm example here does exactly this for recursive deletes.
What happens if my hook script fails?
By default the action goes ahead: a missing script, a crash or a timeout is a non-blocking error, so a broken guard lets everything through. From Claude Code 2.1.295, "onFailure": "block" on a command or HTTP hook makes those failures block instead. The guards built here also block on their own when jq is missing or the input can’t be read.
Do Claude Code hooks work on Windows?
Yes. Command hooks run in Git Bash when it’s installed, otherwise in PowerShell. File paths arrive with backslashes, Git Bash has no jq until you install it, and shell guards should match Bash|PowerShell, because Claude may run commands with the PowerShell tool. Pick the PowerShell option to get scripts that need neither Git Bash nor jq.
Why doesn’t my hook fire?
Check /hooks first. The usual causes are a matcher in the wrong case (bash instead of Bash), an MCP matcher without .*, an if field on an event that isn’t a tool event (the hook then never runs), or invalid JSON in the settings file. Paste your file into the checker on this page to find these.
Are hooks safe to copy from the internet?
A command hook runs with your full user permissions, every time its event fires, so read it like any script before saving it. In an interactive session Claude Code holds back settings hooks until you trust the folder, but -p and SDK runs don’t ask. The examples here never delete anything or install packages; the formatter only rewrites files inside the project.
Related
Related tools
- Claude Code Settings BuilderBuild and check a settings.json with permission rules.
- CLAUDE.md GeneratorGenerate a CLAUDE.md and AGENTS.md for your project.
- MCP Config GeneratorBuild MCP server configs for Claude, Cursor, VS Code and Devin Desktop (Windsurf).
- Claude Code Error DatabaseExact Claude Code error messages with tested fixes.
- Model ID ReferenceCopy current API model IDs for Claude, GPT, Gemini and more.
- AI Token CounterCount tokens for GPT, Claude, Gemini, DeepSeek, Qwen and more.