Back to Article List

Claude Code hooks: Run your own scripts on agent events

Claude Code hooks: Run your own scripts on agent events

You can write "always run prettier after editing" in CLAUDE.md and Claude Code will comply most of the time. Hooks exist for the cases where "most of the time" isn't good enough. A hook is a shell command Claude Code executes at a defined point in the agent's lifecycle, deterministically, every time, regardless of what the model thinks about it. The model asks; a hook enforces. That distinction shapes everything else on this page.

I built the examples below on Ubuntu 24.04 with the 2.1 line of the CLI and jq installed (sudo apt install jq), and checked every field against the official hooks reference. Fair warning: hooks run arbitrary commands with your user's permissions and no confirmation prompt, so treat hook scripts from the internet, including these, as code to read before adopting.

Hook configuration in settings.json

Hooks live under the hooks key in settings.json, at any of the usual levels: ~/.claude/settings.json for all your projects, .claude/settings.json checked into the repo for the team or .claude/settings.local.json for just you in this project. The structure is three levels deep: event, then matcher, then a list of handlers.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh"
          }
        ]
      }
    ]
  }
}

Reading from the outside in: PostToolUse is the event, the matcher "Edit|Write" filters which tool calls trigger it and each entry in the inner hooks array is a handler, usually type: "command". Matchers take exact tool names, pipe-separated alternatives or a regex, with "*" (or an omitted matcher) meaning everything. Tools from MCP servers match too, with patterns like mcp__github__.*. The ${CLAUDE_PROJECT_DIR} placeholder resolves to the project root, so hooks work no matter which subdirectory the session is in.

Hook events worth knowing

The reference lists over two dozen events, from SessionStart all the way to worktree lifecycle events. In practice, five carry most real configurations. PreToolUse fires before a tool call and can block it, which makes it the enforcement point. PostToolUse fires after a tool call succeeds, right for formatters and linters. UserPromptSubmit fires when you submit a prompt, before the model sees it, and can inject context or reject the prompt. SessionStart suits environment setup, and Stop fires when Claude finishes responding, which is your chance to verify the turn achieved something (or to log it). The rest (Notification, PreCompact, SubagentStart, SubagentStop and friends) are situational but the pattern is identical everywhere.

Each hook receives a JSON payload on stdin describing the event. For a PreToolUse firing on Bash, the interesting fields look like this:

{
  "session_id": "abc123",
  "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
  "cwd": "/home/user/acme-api",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite"
  }
}

So a hook script is just a program reading JSON from stdin, doing whatever it wants and signaling its verdict back. Which brings us to the contract.

Hook exit codes: How blocking works

The exit code is the whole protocol. Exit 0 means success; anything printed to stdout goes into the debug log (for a few events, like UserPromptSubmit and SessionStart, stdout becomes context the model sees, a genuinely useful trick). Exit 2 is a blocking error: on events supporting it, the action is stopped and your stderr text is fed back to Claude so it can correct course. Any other exit code is a non-blocking error, noted and moved past.

For decisions richer than block-or-allow, a hook exits 0 and prints JSON to stdout instead:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Writes to migrations/ are hand-reviewed in this repo"
  }
}

permissionDecision takes allow, deny or ask, so a hook can auto-approve known-safe calls or force a confirmation prompt on suspicious ones rather than flatly refusing. Now, the three examples.

Example 1: Auto-format files after edits

Save this as .claude/hooks/format.sh and make it executable with chmod +x:

#!/usr/bin/env bash
FILE=$(jq -r '.tool_input.file_path // empty')

case "$FILE" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.css)
    npx prettier --write "$FILE" >/dev/null 2>&1
    ;;
esac
exit 0

Wire it to PostToolUse with the "Edit|Write" matcher shown in the config above. Every file Claude edits or creates comes out formatted, and the diff noise argument ("the agent keeps reformatting things") dies instantly because formatting happens outside the model entirely. This also removes a whole class of CI failures before they happen; if your pipeline gates on formatting like the one in our GitHub Actions CI/CD guide, the hook means Claude's commits arrive already clean. Note the exit 0 at the end even when prettier fails: I don't want a formatter hiccup blocking the agent, just quiet best-effort formatting.

Example 2: Block risky bash commands

Save as .claude/hooks/guard.sh, executable, matched on Bash under PreToolUse:

#!/usr/bin/env bash
CMD=$(jq -r '.tool_input.command // empty')

BLOCKED='rm -rf /|rm -rf ~|git push --force|drop (table|database)'

if echo "$CMD" | grep -Eiq "$BLOCKED"; then
  echo "Blocked by guard hook: forbidden pattern in: $CMD" >&2
  exit 2
fi
exit 0

Exit 2 stops the command cold, and the stderr line goes back to Claude, which then explains itself and picks another approach. Two honest caveats from running this for a while. Pattern matching is a tripwire rather than a security boundary, since a command can be encoded or built up in ways a regex misses; real isolation comes from permission rules and containers, a subject the dangerously-skip-permissions guide treats properly. And keep the pattern list short, because an overzealous guard hook blocking legitimate work is the fastest way to make an agent useless.

Example 3: Log permission prompts to a file

The third pattern is observation. This one appends a line to a log whenever Claude Code is waiting on a permission decision, which on a remote box tells you the session has stalled and wants you back:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c . >> ~/.claude/notifications.jsonl"
          }
        ]
      }
    ]
  }
}

Inline commands work fine for one-liners like this; scripts earn their file once there's logic. On a desktop, swap the append for notify-send and you've got system notifications when the agent needs attention. The same log-everything trick on PreToolUse with a "*" matcher gives you a complete audit trail of tool calls, which I've found more useful for understanding agent behavior than any amount of scrollback, and it applies to delegated work too since hooks fire for subagent activity, with dedicated SubagentStart and SubagentStop events on top.

Debugging hooks

When a hook doesn't fire or misbehaves, three places tell you why. Run /hooks in a session to browse every configured hook with its event, matcher and source file, which settles "is my config even loaded" immediately. Launch with claude --debug to watch hook execution live, including stderr and non-JSON stdout that are otherwise swallowed. And in the transcript itself, a "hook error" notice marks non-blocking failures, while schema problems in your JSON output land in the debug log.

The failures I hit most: the script isn't executable (chmod +x), the shebang is missing or jq isn't on PATH in the environment the hook runs in. Test scripts by piping a sample payload in by hand before blaming Claude Code:

echo '{"tool_input":{"command":"rm -rf /tmp/x"}}' | .claude/hooks/guard.sh; echo "exit: $?"

Long-running scripts in hooks

My firm position: hooks are for fast, deterministic glue, and anything slow doesn't belong in one. Command hooks get a generous 600-second default timeout, which reads like an invitation to run the full test suite on every Stop event. Don't. Synchronous hooks hold the session while they run, so a two-minute suite after every edit turns a snappy agent into a slideshow, and you'll rip the hook out within a day. Formatters, linters on changed files, pattern checks and log appends all finish in under a second and belong in hooks; test suites belong in CI or in an explicit "run the tests" prompt. If a check is slow but genuinely required, run it on Stop rather than PostToolUse so it fires once per turn instead of once per edit, and set a tight timeout field on the handler so a hung script can't freeze the session. The hooks reference marks which events run synchronously, and that column is the one to check before adding anything with a runtime measured in minutes.

Your idea deserves better hosting

24/7 support 30-day money-back guarantee Cancel anytime
Строк Оплати

VPS.S1

$5.99 Save  17 %
$4.99 Щомісячно
  • 2 vCPU AMD EPYC
  • 2 GB RAMПАМʼЯТЬ
  • 30 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено

VPS.S3

$14.99 Save  33 %
$9.99 Щомісячно
  • 4 vCPU AMD EPYC
  • 6 GB RAMПАМʼЯТЬ
  • 70 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено

EPYC VPS.P1

$8.99 Save  22 %
$6.99 Щомісячно
  • 2 vCPU AMD EPYC
  • 4 GB RAMПАМʼЯТЬ
  • 40 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

EPYC VPS.P2

$16.99 Save  24 %
$12.99 Щомісячно
  • 2 vCPU AMD EPYC
  • 8 GB RAMПАМʼЯТЬ
  • 80 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

EPYC VPS.P4

$29.99 Save  23 %
$22.99 Щомісячно
  • 4 vCPU AMD EPYC
  • 16 GB RAMПАМʼЯТЬ
  • 160 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

EPYC VPS.P5

$39.99 Save  25 %
$29.99 Щомісячно
  • 8 vCPU AMD EPYC
  • 16 GB RAMПАМʼЯТЬ
  • 180 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

EPYC VPS.P6

$59.99 Save  25 %
$44.99 Щомісячно
  • 8 vCPU AMD EPYC
  • 32 GB RAMПАМʼЯТЬ
  • 200 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

EPYC VPS.P7

$69.99 Save  29 %
$49.99 Щомісячно
  • 16 vCPU AMD EPYC
  • 32 GB RAMПАМʼЯТЬ
  • 240 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

Genoa VPS.G2

$24.99 Save  20 %
$19.99 Щомісячно
  • 2 vCPUAMD EPYC Genoa 4-го покоління 9xx4 з 3,25 GHz або подібний, на архітектурі Zen 4. AMD EPYC G4
  • 4 GB DDR5ПАМʼЯТЬ
  • 50 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

Genoa VPS.G4

$44.99 Save  22 %
$34.99 Щомісячно
  • 4 vCPUПроцесор AMD EPYC з виділеними ядрами vCPU, на серверному обладнанні корпоративного класу. AMD EPYC G4
  • 8 GB DDR5ПАМʼЯТЬ
  • 100 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

Genoa VPS.G6

$89.99 Save  22 %
$69.99 Щомісячно
  • 8 vCPUПроцесор AMD EPYC з виділеними ядрами vCPU, на серверному обладнанні корпоративного класу. AMD EPYC G4
  • 16 GB DDR5ПАМʼЯТЬ
  • 200 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

Genoa VPS.G7

$159.99 Save  22 %
$124.99 Щомісячно
  • 8 vCPUПроцесор AMD EPYC з виділеними ядрами vCPU, на серверному обладнанні корпоративного класу. AMD EPYC G4
  • 32 GB DDR5ПАМʼЯТЬ
  • 250 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах. включено
  • Безкоштовний авто бекапМістить один слот резервного копіювання, який можна налаштувати на щоденний, щотижневий або щомісячний запуск.

AMD Ryzen VPS.R1

$16.99 Save  18 %
$13.99 Щомісячно
  • 1 виділений CPU AMD Ryzen 9 7950X з 4,5 GHz або подібний, на архітектурі Zen 4. vCPU
  • 4 GB DDR5ПАМ'ЯТЬ
  • 50 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6 включено Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах.
  • Авто бекап включено

AMD Ryzen VPS.R2

$29.99 Save  17 %
$24.99 Щомісячно
  • 2 виділених CPU AMD Ryzen 9 7950X з 4,5 GHz або подібний, на архітектурі Zen 4. vCPU
  • 8 GB DDR5ПАМ'ЯТЬ
  • 100 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6 включено Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах.
  • Авто бекап включено

AMD Ryzen VPS.R4

$109.99 Save  18 %
$89.99 Щомісячно
  • 8 виділених CPU AMD Ryzen 9 7950X з 4,5 GHz або подібний, на архітектурі Zen 4. vCPU
  • 32 GB DDR5ПАМ'ЯТЬ
  • 400 GB NVMeСХОВИЩЕ
  • Безлімітний трафік
  • IPv4 & IPv6 включено Підтримка IPv6 наразі недоступна у Франції, Фінляндії чи Нідерландах.
  • Авто бекап включено

Frequently asked questions

Can a hook change the input of a tool call instead of blocking it?

On some events, yes. The JSON output schema includes an updatedInput field, so a PreToolUse hook can rewrite a tool's parameters before execution, like redirecting writes into a sandbox directory. The docs list which events support it; block-then-explain with exit 2 stays the simpler pattern when a rewrite isn't strictly needed.