ailiteracynepal 🇳🇵
Text size

Chapter 07 · Section III · 12 min read

Hooks and settings.json

Hooks are how "whenever X, do Y" gets configured — automatic behaviours that fire on Claude Code events, without waiting for a prompt. This is where the tool starts to feel like a real assistant, not a chat window.

Skills give you a shortcut you can invoke. Loops give you a task that repeats. Hooks give you automatic reactions: shell commands that Claude Code runs on its own when a particular thing happens — a tool call finishes, a session starts, a stop signal fires. This is where the CLI stops feeling like a chatbot and starts feeling like a piece of infrastructure.

Where hooks live

Hooks are configured in settings.json — either the user-global one at ~/.claude/settings.json or a project-scoped .claude/settings.json. The shape is roughly:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "npm run lint --silent" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "say 'Claude has stopped'" }
        ]
      }
    ]
  }
}

Each event (PostToolUse, PreToolUse, Stop, SessionStart, etc.) can have multiple hook groups; each group can filter by matcher and run one or more shell commands.

The events

Common events worth knowing:

  • PreToolUse — fires before a tool call is executed. Can block the call (by exiting non-zero) — useful for policy checks.
  • PostToolUse — fires after a tool call finishes. The classic “auto-lint after every edit” hook.
  • SessionStart — fires when a new Claude session opens. Load env vars, print a banner, whatever.
  • Stop — fires when Claude finishes its response. Notification hook.
  • SubagentStop — fires when a subagent completes.

Full list is version-dependent; /help and the docs are the source of truth.

What hooks are for

Well-suited:

  • “Whenever Claude edits a .ts file, run the linter.”
  • “Whenever Claude runs a shell command, log it to a file.”
  • “Whenever a session starts in this repo, source my .env.local.”
  • “When Claude finishes a turn, ring a bell so I stop reading Twitter.”

Poorly-suited:

  • Anything that needs to run in the model’s context. Hooks run as shell commands, outside the conversation. They cannot see Claude’s reasoning; they cannot reply back into the transcript.
  • Anything that should live in a skill or a prompt. If the automation is “always ask the model to do X”, that is a skill or a snippet, not a hook.

Debugging hooks

Two habits pay off:

  • Print, don’t ponder. If a hook is not firing, add echo "hook ran with $CLAUDE_HOOK_MATCHER" >> ~/hook-debug.log at the top. Restart. Look at the file.
  • Read the event payload. Hooks receive event context as environment variables. Knowing what you actually get available (which tool was used, which files, exit code) unlocks most of what you want to do.

The settings.local.json escape hatch

For settings that should not be committed — personal preferences, machine-specific paths — Claude Code respects settings.local.json alongside the regular one. Use it for anything you would not put in the repo.

Check your understanding

Quick check

You want the linter to run automatically every time Claude edits a TypeScript file — no waiting for you to ask. Which primitive is right?