How this skill is triggered — by the user, by Claude, or both
Slash command
/core:hooks [event-or-pattern][event-or-pattern]The summary Claude sees in its skill listing — used to decide when to auto-load this skill
A **hook** is a deterministic action — a shell command, prompt-based LLM check, subagent verification, or HTTP call — that fires at a specific point in Claude Code's lifecycle. Hooks are configured in `settings.json` under a `hooks` block keyed by event name. Use them to enforce rules that should never depend on Claude choosing to follow them: format-on-save, block edits to protected files, val...
examples/async-hook-output.mdexamples/hook-config-output.mdexamples/terminal-sequence-output.mdreference/audit-config-changes.mdreference/auto-approve-permission.mdreference/bash-command-validator.mdreference/block-protected-files.mdreference/concepts.mdreference/events.mdreference/format-on-save.mdreference/inject-context-on-compact.mdreference/io.mdreference/locations.mdreference/matchers.mdreference/notify-on-idle.mdreference/prompt-stop-check.mdreference/troubleshooting.mdreference/types.mdA hook is a deterministic action — a shell command, prompt-based LLM check, subagent verification, or HTTP call — that fires at a specific point in Claude Code's lifecycle. Hooks are configured in settings.json under a hooks block keyed by event name. Use them to enforce rules that should never depend on Claude choosing to follow them: format-on-save, block edits to protected files, validate Bash commands, audit changes, re-inject context after compaction, notify on idle, auto-approve specific permission prompts.
If invoked as /hooks <pattern>, treat $ARGUMENTS as the target event name (PreToolUse, SessionStart, …) or a free-form goal (format on save, block .env edits).
The built-in
/hooksslash command opens a read-only browser of installed hooks. It cannot add, edit, or remove them — only the settings JSON does. Tell the user this if they expect/hooksto be interactive.
Create when the user:
.env, rm -rf, schema drops) before they executeUpdate when the user wants to:
matcher so the hook fires on the right tool callsif rule to filter by tool arguments (Bash(git *), Edit(*.ts))exit 2) to structured JSON (hookSpecificOutput)allowed-tools-style env injection (headers, allowedEnvVars) for HTTP hookscommand hook into a prompt or agent hook (model-based judgment instead of rules)Explain when the user is asking how something works rather than wiring it up. Route to the right reference (see "Explain flow" below).
Propose defaults from the user's request; confirm before writing:
| Decision | Options |
|---|---|
| Event | One of PreToolUse, PostToolUse, PostToolUseFailure, SessionStart, SessionEnd, UserPromptSubmit, UserPromptExpansion, Notification, Stop, StopFailure, PreCompact, PostCompact, SubagentStart, SubagentStop, PermissionRequest, PermissionDenied, ConfigChange, CwdChanged, FileChanged, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, Setup, InstructionsLoaded, PostToolBatch, Elicitation, ElicitationResult, TeammateIdle — see reference/events.md |
| Type | command (shell, default), prompt (single-turn LLM), agent (multi-turn LLM with tools, experimental), http (POST to URL), mcp_tool (call a connected MCP tool). Support varies by event; check reference/types.md before choosing. command hooks accept either exec form (args set, no shell tokenization) or shell form (args omitted) — see reference/types.md |
| Matcher | Empty string fires on every occurrence. Otherwise depends on event: tool name regex (Bash, Edit|Write, mcp__.*), session source (startup, compact), notification kind, etc. — see reference/matchers.md |
if filter | For tool events only: permission-rule syntax to filter by tool name + arguments (Bash(git *), Edit(*.ts)). Requires Claude Code v2.1.85+ |
| Run mode | async: true runs the hook in the background; output is delivered on the next conversation turn. asyncRewake: true is the same but exit code 2 wakes Claude immediately and surfaces stderr (or stdout if stderr is empty). Only command hooks support async; async hooks cannot return decision, permissionDecision, or continue |
| Output mode | Exit code (0 = allow, 2 = block + stderr fed back to Claude) or stdout JSON (hookSpecificOutput, additionalContext, decision). Don't mix |
| Scope | User ~/.claude/settings.json, Project .claude/settings.json (shareable), Local .claude/settings.local.json (gitignored), Managed (org policy), Plugin hooks/hooks.json, or Skill/agent frontmatter |
| Timeout | Defaults: command/http/mcp_tool = 600 s (10 min); UserPromptSubmit lowers them to 30 s; SessionEnd is 1.5 s (override with CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS or per-hook timeout, up to 60 s; plugin timeouts do not raise the budget); prompt = 30 s; agent = 60 s. Override per-hook with "timeout" (seconds) |
Start from a complete recipe in reference/:
Notification hook, desktop alert (macOS / Linux / Windows)PostToolUse + Edit|Write matcher, runs Prettier on the edited filePreToolUse + external script + exit 2PreToolUse + Bash matcher + JSON permissionDecision: "deny"SessionStart with matcher: "compact", stdout becomes Claude's contextConfigChange writes JSON line to audit logPermissionRequest + matcher + hookSpecificOutput.decision.behavior: "allow"Stop hook of type: "prompt", model returns {ok: false, reason} to keep Claude workingFor a minimal output sample (the JSON Claude produces), see examples/hook-config-output.md.
Hooks live under a top-level hooks block. Each event is a key whose value is an array of handler groups. A group has a matcher (regex/literal) and a hooks array of one or more handler objects (type, command / prompt / url, timeout, etc.).
If the settings file already has a hooks key, add the new event as a sibling of existing event keys — do not replace the whole hooks object.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "FILE=$(jq -r '.tool_input.file_path'); npx prettier --write \"$FILE\"" }
]
}
]
}
}
For shell scripts longer than one line, write them to .claude/hooks/<name>.sh, chmod +x, and reference via "$CLAUDE_PROJECT_DIR"/.claude/hooks/<name>.sh so the path resolves regardless of Claude's working directory.
Use path placeholders when bundling hook scripts with a project, plugin, or persistent data directory:
${CLAUDE_PROJECT_DIR} — project root.${CLAUDE_PLUGIN_ROOT} — the plugin's install directory (changes on each plugin update). Use for scripts bundled with a plugin.${CLAUDE_PLUGIN_DATA} — the plugin's persistent data directory (survives plugin updates). Use for installed dependencies and runtime state.Prefer exec form (args set) with placeholders so paths with spaces or special characters need no quoting.
/hooks doesn't show the new hook, restart the session.echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?
Ctrl+O) for the one-line hook summary.claude --debug-file /tmp/claude.log (or /debug mid-session) and tail -f /tmp/claude.log to see every hook's stdin, stdout, stderr, and exit code.Hooks are scoped by the settings file they live in. Walk in this order and ask the user if multiple matches exist:
~/.claude/settings.json (user).claude/settings.json (project, checked in).claude/settings.local.json (project, gitignored)<plugin>/hooks/hooks.json)The /hooks browser shows every installed hook grouped by event with the source file — use it to find the right scope.
Read the settings file and any referenced hook scripts. Don't propose changes blind — a hook that "doesn't fire" might be matched on the wrong event entirely.
| Change | Where to look |
|---|---|
Switch event (PreToolUse ↔ PostToolUse, add SessionEnd, etc.) | reference/events.md |
Adjust matcher (tool regex, session source, MCP tool pattern) | reference/matchers.md |
Add if filter (Bash(git *), Edit(*.ts)) | reference/matchers.md |
Switch between exit 2 and JSON output | reference/io.md |
Add additionalContext, decision, permissionDecision, updatedInput, updatedPermissions | reference/io.md |
Change hook type (command → prompt / agent / http / mcp_tool) | reference/types.md |
Override timeout | reference/types.md |
| Move scope (user ↔ project ↔ local ↔ managed ↔ plugin) | reference/locations.md |
HTTP headers / allowedEnvVars | reference/types.md |
Handle stop_hook_active to avoid Stop block-cap | reference/troubleshooting.md |
| Hook isn't firing / matcher never matches | reference/troubleshooting.md |
| JSON parse errors from leaked shell-profile output | reference/troubleshooting.md |
Edit — preserve sibling event keys, never replace the whole hooks object.local → project (it becomes shared / checked in) or from project → user (it now affects every project).exit 2 to JSON output, remove exit 2 from the script — Claude Code ignores stdout JSON when the script exits 2.headers only resolve if listed in allowedEnvVars; unlisted $VAR references stay empty./hooks that the change is registered under the correct event.Ctrl+O) or debug log.If the user is asking how hooks work rather than wiring one up, skip create/update and route to the right reference:
| Question | Where |
|---|---|
| What's a hook? Why use one over prompting the model? | reference/concepts.md |
| What does each event fire on? Which ones can be blocked? | reference/events.md |
| What input does the hook get on stdin? What output formats exist? | reference/io.md |
How do matchers work? if field? MCP tool patterns? | reference/matchers.md |
command vs prompt vs agent vs http vs mcp_tool — when to use which? | reference/types.md |
Where do hooks live? Precedence? disableAllHooks? | reference/locations.md |
| Hook not firing / parse errors / Stop block cap / debug log | reference/troubleshooting.md |
PreToolUse hook that returns permissionDecision: "deny" blocks the tool even in bypassPermissions or --dangerously-skip-permissions mode. Use this when you genuinely don't want users to bypass."allow" does not override deny rules from settings. Permission deny rules always win.PreToolUse hooks fire on the same call, precedence is deny > defer > ask > allow. All hooks still run to completion — don't rely on one hook's deny to suppress another hook's side effects.exit 2 and JSON output are mutually exclusive. Use exit 2 with stderr for a simple block; use exit 0 + stdout JSON for structured control (additionalContext, updatedPermissions, decision: "block"). Mixing them = JSON ignored.terminalSequence in JSON output. Do not write to /dev/tty; it is unavailable to hooks on all platforms.echo in your ~/.zshrc or ~/.bashrc that always prints will be prepended to your hook's stdout and break JSON parsing. Guard those with if [[ $- == *i* ]]; then ….PostToolUse can't undo. The tool already ran. For pre-execution policy, use PreToolUse.PermissionRequest doesn't fire in -p mode. For automated permission decisions in headless / CI, use PreToolUse instead.updatedInput rewrites race. If two PreToolUse hooks both rewrite the tool's arguments, the last to finish wins — and order is non-deterministic. Don't have more than one hook mutate the same call's input./hooks is read-only — to add/edit/remove, edit the settings JSON directly.chmod +x), or use absolute paths / $CLAUDE_PROJECT_DIR. "command not found" is almost always a path issue.jq isn't installed everywhere — for portable hooks use Python or Node for JSON parsing instead.matcher is case-sensitive and regex-evaluated. bash won't match the Bash tool.if field requires Claude Code v2.1.85+; older versions silently ignore it and fire on every matched call.if only applies to tool events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied). Adding it to any other event prevents the hook from running.stop_hook_active in JSON input and exit 0 if true, or you'll hit the 8-blocks-in-a-row cap.hookSpecificOutput does.bypassPermissions mode in a PermissionRequest hook's updatedPermissions only takes effect if the session was launched with bypass already available.terminalSequence (OSC 0/1/2/9/99/777 or bare BEL only) to fire desktop notifications or set window titles. /dev/tty is unavailable to hooks since v2.1.139.hookSpecificOutput requires hookEventName set to the firing event name. Without it, the JSON is inert and Claude Code falls back to the default behavior.command + args. Identical HTTP hooks are auto-deduplicated by url. A subtle whitespace or env-var difference defeats dedup.exit 2 blocks itcommand, prompt, agent, http, mcp_tool — fields, timeouts, trade-offsif field, MCP tool patternsdisableAllHookshooks block in settings.jsonPostToolUse hook delivered on the next turnterminalSequence JSON for an OSC 777 desktop notificationreference/ — complete working hook recipes for each common pattern. Read the matching one before drafting a new hook.
npx claudepluginhub shoto290/shoto --plugin coreGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.