Hooks are one of Claude Code’s least-known and most powerful features. They let you run shell commands automatically before or after Claude uses any tool. Guardrails without friction.

What hooks are and what they’re for

A hook is a shell command that Claude Code runs in response to agent events. Available events:

  • PreToolUse — before Claude uses a tool
  • PostToolUse — after Claude uses a tool
  • Notification — when Claude wants to notify you of something
  • Stop — when Claude finishes a task

Real use cases: run the linter after each edit, take a git snapshot before destructive changes, block edits on protected files, send Slack notifications when the agent finishes.

Configuring hooks in settings.json

Hooks are configured in ~/.claude/settings.json (global) or .claude/settings.json (project):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && npx eslint --fix $CLAUDE_FILE_PATHS 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

The matcher is a regex matching the tool name. Write|Edit captures both editing tools.

Available environment variables

Claude Code exposes useful variables in each hook:

  • $CLAUDE_PROJECT_DIR — project root directory
  • $CLAUDE_FILE_PATHS — files affected by the tool (space-separated)
  • $CLAUDE_TOOL_NAME — name of the tool that ran
  • $CLAUDE_SESSION_ID — current session ID

Blocking protected files

You can use the exit code to block actions. If the hook returns exit code 2, Claude Code cancels the action and shows stderr as an error message:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_FILE_PATHS\" | grep -q 'production.env\\|\\.secrets'; then echo 'Protected file: edit blocked' >&2; exit 2; fi"
          }
        ]
      }
    ]
  }
}

Auto-commit hook after each task

A useful pattern: auto-commit when Claude finishes, to have a granular history of each agent change:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && git diff --quiet || git add -A && git commit -m 'chore: claude agent checkpoint'"
          }
        ]
      }
    ]
  }
}

Hooks turn Claude Code into an auditable, controlled agent. With a few shell commands you define exactly what it can and cannot do in your project.