Claude Code Hooks Builder

Pick an event, point it at a command, copy the JSON. No syntax spelunking — the fragment below is always valid, and the complete hooks reference lives right under the tool.

The command receives the event's JSON on stdin (see what your hook receives). Exit 0 = fine, exit 2 = block (where the event allows it).

Your settings.json fragment

"hooks": {}

This is the "hooks" key only — paste it inside the top-level { } of your settings file (add a comma if other keys follow). Already have a "hooks" key? Merge the event arrays. Building the rest of the file — permissions, env, model? That's the settings.json Builder's job.

Reference verified against Claude Code v2.1.205 — July 8, 2026. Hooks change between releases; if something below disagrees with your install, trust claude --version and the official reference, then tell us.

Claude Code hooks, explained

A hook is a standing order: a shell command Claude Code runs automatically, every time, at a fixed point in its lifecycle. Before a tool call. After a file edit. When a session starts. When Claude thinks it's finished. You configure hooks once under the "hooks" key in a settings file, and from then on the behavior is deterministic — unlike an instruction in CLAUDE.md, which Claude should follow, a hook always runs.

That determinism is the whole point. "Please run prettier after editing" works until it doesn't; a PostToolUse hook on Edit|Write works every time, forever, without spending a single token reminding anyone. Hooks are how you turn a habit into infrastructure.

One warning before anything else: hooks run with your full user permissions, without asking. A hook is code you have agreed to execute unattended. Read every command before you register it — especially ones you copied from the internet, including this page.

Quick start

The smallest useful hook — log every shell command Claude runs, so you always have an audit trail. In ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/bash-log.txt"
          }
        ]
      }
    ]
  }
}

Reading it inside-out: when the PreToolUse event fires and the tool name matches Bash, run this command. The hook receives the event as JSON on stdin — here jq plucks out the command text and appends it to a log. That's the entire model. Everything else on this page is detail on the three questions every hook answers: when (the event), on what (the matcher), and do what (the hook entry).

Every hook event

Claude Code fires hooks at thirty points in its lifecycle. You will use five of them constantly and the rest rarely — but "rarely" is exactly when you'll want this table. The Blocks? column says whether the hook can veto the action by exiting with code 2 (details under exit codes).

The core five

EventFiresBlocks?
PreToolUseBefore any tool call executes. The gatekeeper event: inspect the call, allow it, rewrite it, or block it.Yes
PostToolUseAfter a tool call succeeds. The janitor event: format the file that was just written, run the linter, regenerate types.No — it already ran
UserPromptSubmitWhen you submit a prompt, before Claude sees it. Inject context, validate, or block the prompt entirely.Yes
StopWhen Claude finishes responding. Exit 2 sends Claude back to work with your stderr as instructions — the "not done yet" event.Yes
SessionStartWhen a session begins or resumes. stdout becomes context Claude starts with — git status, open tickets, yesterday's notes.No

Tool and permission events

EventFiresBlocks?
PermissionRequestWhen a permission dialog is about to appear. Auto-answer it from a hook instead of clicking.Yes
PermissionDeniedAfter a tool call is denied by the permission system.Yes
PostToolUseFailureAfter a tool call fails — the error-path twin of PostToolUse.No
PostToolBatchAfter a full batch of parallel tool calls resolves, before the next model call.No

Session lifecycle

EventFiresBlocks?
SessionEndWhen a session terminates. Matcher tells you why: clear, logout, otherNo
SetupOn --init/--init-only/--maintenance runs — repo bootstrap hooks.No
PreCompact / PostCompactAround context compaction. Matcher: manual or auto.No
NotificationWhen Claude Code sends a notification (permission prompt, idle, agent done…). The "ping my phone" event.No
StopFailureWhen a turn ends due to an API error. Matcher is the error type (rate_limit, server_error…).No
MessageDisplayWhen assistant message text is displayed.No
UserPromptExpansionWhen a typed command expands into a prompt, before Claude sees it. Matcher is the command name.Yes

Agents, tasks, and teammates

EventFiresBlocks?
SubagentStartWhen a subagent spawns. Matcher is the agent type.No
SubagentStopWhen a subagent finishes — exit 2 keeps it working, same as Stop.Yes
TaskCreated / TaskCompletedWhen a task is created / marked complete.No
TeammateIdleWhen an agent-team teammate is about to go idle.No

Environment and config

EventFiresBlocks?
ConfigChangeWhen a settings file changes mid-session. Exit 2 rejects the change — a tamper alarm for your config.Yes
FileChangedWhen a watched file changes on disk. Matcher is literal filenames (.env|.envrc), not regex.No
CwdChangedWhen the working directory changes.No
InstructionsLoadedWhen CLAUDE.md or rules files load into context.No
WorktreeCreate / WorktreeRemoveAround git-worktree isolation. Create can be failed by a non-zero exit.Create: yes
Elicitation / ElicitationResultAround an MCP server requesting user input. Matcher is the server name.No

The configuration shape

Every hook config has the same three levels of nesting, and most first-attempt failures are a missing level. Memorize the skeleton:

"hooks": {                          // 1. the event map
  "PreToolUse": [                   // 2. a list of matcher groups per event
    {
      "matcher": "Bash",            //    which occurrences this group cares about
      "hooks": [                    // 3. the hooks that run when it matches
        { "type": "command", "command": "./check.sh", "timeout": 30 }
      ]
    }
  ]
}

Notes that save debugging time:

Matchers

The matcher string is interpreted in one of three ways:

What the matcher compares against depends on the event:

EventMatcher compares against
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied Tool name — Bash, Edit, Write, Read, Glob, Grep, WebFetch, Agent, or MCP tools as mcp__<server>__<tool>
SessionStartstartup · resume · clear · compact
SessionEndclear · resume · logout · prompt_input_exit · bypass_permissions_disabled · other
Notificationpermission_prompt · idle_prompt · auth_success · elicitation_dialog · elicitation_complete · elicitation_response · agent_needs_input · agent_completed
PreCompact, PostCompactmanual · auto
SubagentStart, SubagentStopAgent type — general-purpose, Explore, or your custom agent names
ConfigChangeuser_settings · project_settings · local_settings · policy_settings · skills
StopFailureError type: rate_limit · overloaded · server_error · billing_error · max_output_tokens · and friends
FileChangedLiteral filenames separated by |not regex
Setupinit · maintenance
InstructionsLoadedsession_start · nested_traversal · path_glob_match · include · compact
Elicitation, ElicitationResultMCP server name
UserPromptExpansionCommand / skill name
Stop, UserPromptSubmit, PostToolBatch, CwdChanged, TaskCreated, TaskCompleted, TeammateIdle, MessageDisplay, WorktreeCreate, WorktreeRemoveNo matcher — always fires

What your hook receives

Every hook gets one JSON object on stdin. The common fields:

{
  "session_id": "…",
  "prompt_id": "…",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/directory",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse"
}

Each event adds its own fields — the ones you'll actually parse:

EventExtra fields
PreToolUsetool_name, tool_input (the tool's arguments — e.g. .tool_input.command for Bash, .tool_input.file_path for Edit/Write)
PostToolUsetool_name, tool_input, tool_output
PostToolUseFailuretool_name, tool_input, error
UserPromptSubmitprompt_text
Stoplast_assistant_message, stop_hook_active (true once your Stop hook has blocked repeatedly — check it to avoid infinite loops)
SessionStartsource, model
FileChangedfile_path, change_type
SubagentStart / SubagentStopagent_type (+ stop_reason on stop)

jq is the standard way in: jq -r '.tool_input.file_path' and you're off. For anything longer than a one-liner, write a script that reads stdin and point the hook at the script.

What your hook says back: exit codes and JSON

Hooks talk back through their exit code, and optionally through JSON on stdout.

For decisions richer than block/allow, print JSON on stdout with exit 0:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Writes to /etc are off-limits in this repo."
  }
}

The fields worth knowing: permissionDecision (allow / deny / ask / defer) pre-answers the permission check on PreToolUse; updatedInput rewrites the tool's arguments before it runs; additionalContext injects text into Claude's context; continue: false with a stopReason halts processing outright; and suppressOutput: true keeps your hook's stdout out of the transcript. A top-level "decision": "block" with a "reason" is the JSON equivalent of exit 2 on Stop and PostToolUse.

Where hooks live

FileScopeUse it for
~/.claude/settings.jsonYou, everywherePersonal habits: logging, notifications, formatters you always want
.claude/settings.jsonOne project, committedTeam rules: lint gates, protected paths, project conventions
.claude/settings.local.jsonOne project, gitignoredYour machine-specific overrides
Managed policy settingsWhole orgAdmin-enforced guardrails

Plugins can ship hooks in hooks/hooks.json, and skills and agents can carry hooks in their frontmatter that apply only while they're active. All sources merge; to turn everything off temporarily, set "disableAllHooks": true. To build the settings file these fragments paste into — permissions, env, model, the works — use the settings.json Builder.

Worked examples

Each of these is a complete "hooks" fragment — the same output the builder produces. Load puts it in the builder above so you can tweak before copying.

Auto-format after every edit

The classic. Prettier runs on every file Claude edits or writes, the moment it happens. Swap in black, gofmt, or rustfmt to taste.

"hooks": {
  "PostToolUse": [
    {
      "matcher": "Edit|Write",
      "hooks": [
        {
          "type": "command",
          "command": "jq -r '.tool_input.file_path // empty' | xargs -r npx prettier --write --ignore-unknown"
        }
      ]
    }
  ]
}

Block dangerous rm commands

A PreToolUse guard on Bash: if the command smells like a recursive delete of home or root, exit 2 — the call is blocked and Claude is told why. Deterministic, unlike hoping the model declines.

"hooks": {
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "jq -r '.tool_input.command' | grep -qE 'rm .*-[a-z]*r[a-z]*f|rm .*-[a-z]*f[a-z]*r' && { echo 'Blocked: recursive force delete. Ask the human.' >&2; exit 2; } || exit 0"
        }
      ]
    }
  ]
}

Log every Bash command

An audit trail of everything Claude runs, one line per command, timestamped. Costs nothing, and the one day you need it you'll really need it.

"hooks": {
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "jq -r '\"[\\(now | todate)] \\(.tool_input.command)\"' >> ~/.claude/bash-log.txt"
        }
      ]
    }
  ]
}

Desktop notification when Claude needs you

You started a long task and switched windows. This macOS example pings you the moment Claude is waiting on a permission prompt; on Linux, swap in notify-send.

"hooks": {
  "Notification": [
    {
      "matcher": "permission_prompt",
      "hooks": [
        {
          "type": "command",
          "command": "osascript -e 'display notification \"Claude Code is waiting on a permission prompt\" with title \"Claude Code\"'"
        }
      ]
    }
  ]
}

Play a sound when Claude finishes

"hooks": {
  "Stop": [
    {
      "hooks": [
        {
          "type": "command",
          "command": "afplay /System/Library/Sounds/Glass.aiff"
        }
      ]
    }
  ]
}

Inject git context at session start

On SessionStart, whatever the hook prints becomes context Claude begins with — so every session opens already knowing your branch and dirty files.

"hooks": {
  "SessionStart": [
    {
      "matcher": "startup",
      "hooks": [
        {
          "type": "command",
          "command": "echo \"Branch: $(git branch --show-current 2>/dev/null). Uncommitted: $(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') files.\""
        }
      ]
    }
  ]
}

FAQ

My hook isn't firing. What do I check first?

In order: (1) valid JSON — one trailing comma kills the whole file silently; (2) the nesting — event → matcher group → hooks array, all three levels; (3) the matcher — bash doesn't match Bash, matchers are case-sensitive; (4) run /hooks inside Claude Code to see what's actually registered. Settings edits are normally picked up live, but a restart forces a reload.

Hooks vs. CLAUDE.md instructions — when do I use which?

Instructions are requests; hooks are law. Use CLAUDE.md for judgment calls ("prefer small commits"), hooks for anything that must happen every time (formatting, logging, blocking a path). If you've written the same reminder twice, it wants to be a hook.

Can a Stop hook loop forever?

It can try — a Stop hook that always exits 2 sends Claude back to work indefinitely. Claude Code caps consecutive blocks and sets stop_hook_active: true in your hook's stdin once a block streak is running; well-behaved Stop hooks check it and exit 0.

Do hooks work inside subagents?

Yes — tool events fire in subagents too, and the stdin JSON carries agent_id and agent_type so you can tell. SubagentStart and SubagentStop let you hook the subagent lifecycle itself, with the agent type as the matcher.

Is any of this a security risk?

The hooks mechanism is a security feature — it's how you enforce guardrails. The commands are the risk: they run unattended with your permissions. Treat a hook entry like a cron job — read it, understand it, and be suspicious of anything you didn't write, including snippets from this page.