DEV Community

Cover image for Which Claude Code Hook Do You Need? A Decision Guide
ShipWithAI
ShipWithAI

Posted on • Originally published at shipwithai.io

Which Claude Code Hook Do You Need? A Decision Guide

Claude Code has 4 hook handler types (command, prompt, agent, http) and 21 lifecycle events. Most developers default to command hooks on PreToolUse. This decision guide helps you pick the right type for the right event, and tells you which 3 to implement first.


Two configs. Same goal: block a force push to main. Different reliability:

# Command hook (deterministic, <5ms)
COMMAND=$(jq -r '.tool_input.command // empty' < /dev/stdin)
if echo "$COMMAND" | grep -qE 'git push.*(--force|-f).*main'; then
    echo "BLOCKED: force push to main" >&2
    exit 2
fi
Enter fullscreen mode Exit fullscreen mode
// Prompt hook (non-deterministic, 300-2000ms)
{
  "type": "prompt",
  "prompt": "Block this if it looks like a force push to a production branch"
}
Enter fullscreen mode Exit fullscreen mode

The command hook is 5 lines of bash. It runs in under 5ms. It catches every git push --force main without exception.

The prompt hook calls an LLM. It takes 300-2000ms. It might decide --force-with-lease is safe enough to allow.

Both are "hooks." Choosing the wrong type turns a guardrail into a suggestion. CLAUDE.md instructions achieve 70-90% compliance. Hooks achieve 100% — but only when you pick the right one.


What are the 4 Claude Code hook handler types?

Each type trades speed for intelligence differently. Pick the wrong type and your 100% guardrail drops to a probabilistic suggestion.

Handler Speed Deterministic? Codebase Access? Best For
command <5ms Yes No (stdin only) Guardrails, formatting, logging
prompt 300-2000ms No No Nuanced decisions on Stop
agent 2-10s No Yes (full tools) Deep verification, architecture
http 50-500ms Yes (your server) No Team policies, centralized audit

Command hooks are shell scripts. They read JSON from stdin, run fast, and return deterministic results. Use them for anything you can express as a string match, path check, or regex.

Prompt hooks call an LLM to make a judgment call. Only use them when the decision genuinely requires reasoning, like evaluating subagent output quality on SubagentStop.

Agent hooks spawn a full Claude Code session that can read files, search code, and run tools. Reserve them for verification tasks that need codebase context.

HTTP hooks POST to your server. Useful for centralized team policies and audit logging.

The critical rule: never use prompt-based hooks for safety boundaries. Prompt hooks involve LLM judgment, and LLMs can be wrong. Safety boundaries need deterministic command hooks.


When should you use CLAUDE.md vs a hook vs both?

Use CLAUDE.md for conventions the agent should follow. Use hooks for rules the agent must never break. Use both when you want the agent to understand WHY while the hook enforces WHAT.

Is this a HARD constraint (must NEVER be violated)?
├── YES → Can you test it with a string/path/regex check?
│         ├── YES → Command hook (PreToolUse)
│         └── NO  → Does it need codebase context?
│                   ├── YES → Agent hook
│                   └── NO  → Prompt hook or HTTP hook
└── NO  → Is it a preference or convention?
              ├── YES → CLAUDE.md (~70-90% compliance)
              └── NO  → Is it a repeatable workflow?
                        ├── YES → Skill or .claude/commands/
                        └── NO  → You probably don't need it
Enter fullscreen mode Exit fullscreen mode

When should you use both? When the constraint is structural (hook enforces it) but the agent also benefits from understanding the reasoning:

  • Hook: PreToolUse blocks git push --force to main
  • CLAUDE.md: "We use --force-with-lease instead of --force because a force push overwrote a teammate's commits in March 2026"

The hook prevents the bad action. The CLAUDE.md helps the agent choose the right alternative.


Which hook events should you implement first?

Start with 3 events in this order:

Priority Event Handler What It Does Setup Time
1st PreToolUse command Block dangerous actions 15 min
2nd PostToolUse command Auto-format, log actions 20 min
3rd Stop agent Verify work before done 30 min
4th SessionStart command Load env vars, context 10 min
5th SubagentStop prompt Validate subagent output 20 min
6th PermissionRequest command Auto-approve safe patterns 15 min
7th PreCompact command Preserve context on compact 15 min

Your first hook — a PreToolUse command hook that blocks force pushes:

#!/bin/bash
# .claude/hooks/block-force-push.sh
# Blocks git push --force and -f to main/master/production

COMMAND=$(jq -r '.tool_input.command // empty' < /dev/stdin)

if echo "$COMMAND" | grep -qE 'git push.*(--force|-f)' && \
   echo "$COMMAND" | grep -qE '(main|master|production)'; then
    echo "BLOCKED: force push to protected branch" >&2
    exit 2
fi

exit 0
Enter fullscreen mode Exit fullscreen mode

Register it in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/block-force-push.sh"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

How do you handle multiple hooks on the same event?

Hooks on the same event run in definition order. For PreToolUse, the strictest decision wins: deny beats defer, defer beats ask, ask beats allow. If any hook denies, the action is blocked regardless of what other hooks return.

Chain hooks from fastest to slowest to minimize latency:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bash .claude/hooks/block-force-push.sh" },
          { "type": "command", "command": "bash .claude/hooks/validate-paths.sh" },
          { "type": "command", "command": "bash .claude/hooks/log-action.sh" }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Decision precedence hierarchy:

deny   → Action blocked. Feedback sent to model.
defer  → Action paused (headless mode). External UI resumes.
ask    → User prompted for confirmation.
allow  → Action proceeds. Skips built-in permission check.
(none) → Default behavior. Built-in permission check runs.
Enter fullscreen mode Exit fullscreen mode

What are the most common hook mistakes?

Three mistakes account for most "my hook doesn't work" reports:

Exit code cheat sheet

Exit Code Meaning Model Sees Feedback?
0 Success (parse JSON from stdout) Yes, if JSON provided
2 Block action (stderr becomes feedback) Yes
Any other Silent error (logged in verbose only) No

The exit 1 vs exit 2 distinction is the #1 gotcha. Exit 1 means "my hook crashed." Claude Code logs it quietly and continues. Exit 2 means "I'm deliberately blocking this action."

Debug workflow

Test any hook manually:

echo '{"tool_name":"Bash","tool_input":{"command":"git push --force main"}}' \
    | bash .claude/hooks/block-force-push.sh
echo "Exit code: $?"
Enter fullscreen mode Exit fullscreen mode

If the hook doesn't run at all, check:

  • Path correct? Command path is relative to project root, not the hooks directory
  • Matcher correct? "matcher": "Bash" matches the tool name, not the command content
  • Settings level? Project .claude/settings.json overrides user ~/.claude/settings.json
  • File executable? Run chmod +x .claude/hooks/your-hook.sh
  • JSON valid? A syntax error in settings.json silently disables all hooks

FAQ

What are the 4 Claude Code hook handler types?

Command (shell scripts, <5ms, deterministic), prompt (LLM judgment, 300-2000ms), agent (multi-turn verification with codebase access, 2-10s), and http (webhooks, 50-500ms). Use command hooks for guardrails and formatting. Use prompt or agent hooks for nuanced decisions that require reasoning.

Should I use CLAUDE.md or a hook for security rules?

Hooks. CLAUDE.md instructions achieve 70-90% compliance because they compete with 200K tokens of context. A PreToolUse command hook achieves 100% compliance because it runs outside the LLM's reasoning chain. Use CLAUDE.md to explain WHY. Use hooks to enforce WHAT.

What is the difference between PreToolUse and PostToolUse hooks?

PreToolUse runs BEFORE a tool executes and can block it (exit code 2) or modify its input. PostToolUse runs AFTER execution and cannot undo the action, but it can auto-format code, log what happened, or inject feedback. PreToolUse for prevention, PostToolUse for reaction.

Can Claude Code hooks run in headless mode?

Yes. All hook types work in headless mode (claude -p). PreToolUse hooks can return permissionDecision: "defer" to pause execution for external UI collection. This makes hooks fully compatible with CI/CD pipelines and SDK-based workflows.


Try it now: Copy the force-push blocker script into .claude/hooks/block-force-push.sh, register it in .claude/settings.json, make it executable with chmod +x, and test it with the debug command above. Verify exit code 2. You now have one production-ready guardrail.

Which hook event would you implement first? Drop it in the comments.


Originally published on ShipWithAI. I write about Claude Code workflows, AI-assisted development, and shipping software faster with structured AI.

Top comments (0)