little cups of claude

tips and techniques for Claude and Claude Code

#2: Hook Basics 🪝

Today we'll look at one of the best ways to make Claude Code behave the same way every time: hooks.

Hooks run our own code at key moments in a Claude Code session. Instead of hoping Claude remembers to format a file or skip a protected directory, we make it happen automatically, on every matching event.

Let's dive in.

Setting Them Up

Hooks live in our settings files. The user file (~/.claude/settings.json) applies to every project, .claude/settings.json is shared with the team, and .claude/settings.local.json stays on our machine. Plugins, skills, and subagents can bring their own hooks too.

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

This one runs Prettier every time Claude Code edits or writes a file. Every hook gets a JSON payload on stdin, so jq pulls the file path right out of it. Neat!

The Events

Claude Code has more than 30 hook events now. These are the ones we'll reach for most:

  • PreToolUse fires before a tool call and can block it
  • PostToolUse fires after a tool call succeeds
  • UserPromptSubmit fires when we send a prompt, before Claude sees it
  • SessionStart fires when a session begins or resumes
  • Stop fires when Claude finishes responding, and can make it keep going
  • Notification fires when Claude Code wants our attention
  • PreCompact and PostCompact wrap context compaction
  • SubagentStart and SubagentStop wrap subagent runs

There are also events for config changes, file changes on disk, worktrees, model switches, permission requests, and MCP elicitations.

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt|idle_prompt",
        "hooks": [{ "type": "command", "command": "say 'HAL needs us'" }]
      }
    ]
  }
}

That one uses macOS say so we hear it when Claude is waiting on us.

Five Handler Types

Each hook handler has a type:

  • command runs a shell command (the most common)
  • http POSTs the event JSON to a URL
  • mcp_tool calls a tool on one of our MCP servers
  • prompt asks a Claude model for a quick yes/no style decision
  • agent spawns a subagent that can read files before deciding (still experimental)

Matchers And if

The matcher field filters what a hook fires on. For tool events it matches the tool name, so Bash, Edit|Write, and mcp__.* all work. For more precise filtering on tool events, the if field takes one permission rule:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git push *)",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-branch.sh"
          }
        ]
      }
    ]
  }
}

Note that if goes on the handler, next to type. It holds exactly one rule, so two conditions means two handlers.

Exit Codes

How our hook exits decides what happens next:

  • Exit 0: success, and the action goes ahead
  • Exit 2: a blocking error. On a PreToolUse hook the tool call is stopped, and our stderr goes back to Claude as the reason
  • Anything else: a non-blocking error, and the action still goes ahead
#!/usr/bin/env python3
import json, sys

data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "")

if any(p in path for p in [".env", "bun.lock", ".git/"]):
    print(f"Blocked: {path} is protected", file=sys.stderr)
    sys.exit(2)

Claude reads that message and tries another approach.

Watch Out

Exit code 1 does not block anything, even though it's the usual Unix failure code. If a hook is enforcing a policy, use exit 2.

The timeout field is in seconds, and command hooks default to 600. All matching hooks for an event run in parallel. Edits to hooks in our settings files are picked up automatically, and /hooks opens a read-only browser showing every hook and where it came from.

More info about hooks can be found at the Claude Code docs.

Share

Share on Threads

Similar Cups