ailiteracynepal 🇳🇵
Text size

Chapter 03 · Section III · 12 min read

3.3 Hooks: lifecycle events and permission gates

Hooks are the automation surface — shell commands Claude Code runs on lifecycle events. The event catalog, the matcher shape, and how PreToolUse hooks double as programmable permission gates.

Hooks let you say “whenever X happens, run this command.” Claude Code fires an event; your hook is a small shell command that reads context from env vars and either returns silently, prints something into the transcript, or blocks the underlying action.

Lifecycle events

The events you will actually use:

EventFires when
SessionStartA session opens
UserPromptSubmitYou send a message
PreToolUseBefore a tool call runs; can block
PostToolUseAfter a tool call finishes
NotificationClaude wants your attention (e.g. permission prompt)
StopClaude finishes its response
SubagentStopA subagent completes
PreCompactBefore context compaction

Full list is version-dependent; claude hooks list-events is the source of truth.

The config shape

Hooks live in settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "npm run lint --silent" }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "~/.claude/hooks/bash-audit.sh" }
        ]
      }
    ]
  }
}
  • matcher — a regex matched against the tool name (or event-specific field). Omit to fire on every occurrence.
  • command — the shell to run. Receives event context as env vars: CLAUDE_HOOK_EVENT, CLAUDE_TOOL_NAME, CLAUDE_TOOL_INPUT, and event-specific ones.

PreToolUse as a permission gate

A PreToolUse hook can approve, block, or ask, by its exit code:

  • Exit 0 — proceed with the tool call.
  • Exit 2 — block. Claude sees the block and adapts.
  • Any other non-zero — treated as an error; call is refused.

This is powerful: you can write a policy in real code that no static allowlist could express. Example — block any Bash command that writes to /etc or contains a token:

#!/bin/bash
set -e
CMD="$CLAUDE_TOOL_INPUT_COMMAND"
if echo "$CMD" | grep -qE '(/etc/|sk-ant-api)'; then
  echo "blocked: sensitive target or secret" >&2
  exit 2
fi
exit 0

Common patterns

  • Auto-lint on save. PostToolUse matched on Edit|Write runs the linter.
  • Log every Bash command. PreToolUse matched on Bash appends the command to a session log.
  • Notification bell. Stop runs say "done" or notify-send.
  • Rate-limit expensive tools. PreToolUse on WebSearch checks a counter, blocks after N calls.

Check your understanding

Quick check

You want to enforce a policy that no Bash command is allowed to modify anything in `/etc`. Which hook + exit code combination is right?