Hooks and deterministic automation

Run your own commands at lifecycle events, block a dangerous tool call with an exit code, and format every edited file without asking the model.

Lifecycle events and the settings shape

Hooks are shell commands, HTTP calls or prompts that run at fixed points in a session. They are deterministic: unlike an instruction in CLAUDE.md, a hook always fires. That makes them the right tool for anything that must not depend on the model's willingness.

EventFiresTypical use
PreToolUseBefore a tool runs; can block itRefuse writes to protected paths
PostToolUseAfter a tool succeedsRun a formatter on edited files
UserPromptSubmitWhen you submit a promptInject the current branch or ticket id
SessionStartWhen a session beginsLoad environment context
StopWhen the agent finishes respondingRun the test suite and report
SubagentStopWhen a subagent finishesLog what a delegated task concluded
PreCompactBefore history is summarisedArchive the transcript
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
  • Configuration lives in .claude/settings.json (shared) or .claude/settings.local.json (yours).
  • matcher is a regular expression over the tool name, so Edit|Write covers both.
  • The hook receives a JSON payload on stdin describing the event, including the tool name and its input.

Blocking a call

#!/usr/bin/env bash
# .claude/hooks/guard-secrets.sh
# Refuse any write that touches a secret file or hardcodes a key pattern.
set -euo pipefail

payload=$(cat)
path=$(printf '%s' "$payload" | jq -r '.tool_input.file_path // ""')
content=$(printf '%s' "$payload" | jq -r '.tool_input.content // .tool_input.new_string // ""')

if [[ "$path" == *".env"* || "$path" == secrets/* ]]; then
  echo "Blocked: $path is a protected path." >&2
  exit 2
fi

if printf '%s' "$content" | grep -Eq 'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}'; then
  echo "Blocked: looks like a hardcoded credential." >&2
  exit 2
fi

exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/guard-secrets.sh" }
        ]
      }
    ]
  }
}
⚠️
Exit code 2 is the blocking signal: stderr is fed back to the model as an explanation, so write the reason there. Any other non-zero exit is reported as a hook failure and the tool call still proceeds - which means a hook that crashes silently approves everything it was meant to stop. Test it by trying to write a file it should reject.

Hook safety and debuggability

  • A hook runs with your user's privileges. Treat a shared settings file as executable code: review changes to it like you would review a shell script from a stranger.
  • Read the payload from stdin; do not interpolate tool input into a shell string without quoting.
  • Keep hooks fast. A hook on PostToolUse runs on every edit; a five-second formatter on a busy session is five seconds times every file.
  • Log to a file if you need to know whether a hook fired - /hooks shows what is configured, not what ran.
  • Prefer PostToolUse for formatting and PreToolUse only for genuinely blocking checks, to avoid interrupting normal edits.
# verify that stdin really is what you think it is
echo '{"tool_name":"Write","tool_input":{"file_path":".env","content":"A=1"}}' \
  | .claude/hooks/guard-secrets.sh ; echo "exit=$?"

# then force the whole run to fail when the guard is wrong
claude -p "append DEBUG=1 to .env" --allowedTools "Edit" ; echo "exit=$?"

FAQ

Hooks or CLAUDE.md instructions?
If the requirement is absolute - never write to this directory, always format after editing - use a hook. Instructions are probabilistic; hooks are not. Reserve instructions for judgement calls the model should make, and hooks for rules that must hold every time.
Why does my hook not fire at all?
Check the matcher's exact tool name, that the settings file is in the directory you launched from, and restart the session after editing it. Hooks are read at startup, so a running session will not see a newly added hook.

Permissions and safety with agents Headless mode, scripts and CI automation

Last refreshed 2026-09-18.