Claude Code Hooks: A Practical Guide to Automating AI Agent Workflows

Claude Code hooks let you run shell commands automatically at key points in the agent loop — before tools run, after edits, when sessions end. A working developer's guide with real configurations.

Claude Code Hooks: A Practical Guide to Automating AI Agent Workflows

Claude Code hooks are the closest thing the AI coding world has to git hooks — and they solve a similar class of problem. You want certain things to happen automatically every time the agent does X, without trusting the model to remember.

A model can be told "always run the linter after editing TypeScript." Most of the time it will. Sometimes it won't, because the prompt gets long or the task gets complicated or the model just decides this particular case is the exception. Hooks remove that variance. The shell runs the command. The agent has no say.

This is more important than it sounds. The difference between "the agent usually formats code" and "the agent always formats code" is the difference between something you can rely on and something you have to babysit.

What Hooks Actually Are

Claude Code hooks are shell commands configured in your settings file that fire at specific lifecycle events in the agent loop. They run in your shell environment, with your tools, and they can either run as side effects or block the agent's action.

The hook events worth knowing:

  • PreToolUse — runs before a tool call executes. Can inspect the tool and arguments and block the call.
  • PostToolUse — runs after a tool call completes. Pure side effect, can't block.
  • UserPromptSubmit — runs when you submit a prompt. Can inject context into the conversation or block the prompt.
  • Stop — runs when the agent stops. Useful for cleanup or notifications.
  • SessionStart — runs when a new session begins. Useful for environment setup.
  • Notification — runs when Claude Code shows a notification (waiting for input, permission requested).

Each fires with a structured JSON payload describing what's happening, which your hook command can parse to make decisions.

Where Hooks Are Configured

Hooks live in settings.json files, either user-scoped (~/.claude/settings.json) or project-scoped (.claude/settings.json inside your repo). Project-scoped is usually right — hooks tend to be specific to a codebase's conventions.

A minimal example:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint:fix --silent || true"
          }
        ]
      }
    ]
  }
}

That's it. Every time the agent edits or writes a file, the linter runs and auto-fixes what it can. No prompt engineering, no "please remember to lint," no inconsistency.

Hook Patterns That Earn Their Place

Not every workflow needs hooks. The ones that pay off are workflows where you want guaranteed behavior the model doesn't have to think about. Some specific patterns:

Format and Lint After Edits

The classic case. After any file mutation, run the formatter.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && pnpm format && pnpm lint:fix"
          }
        ]
      }
    ]
  }
}

Two things to notice. First, $CLAUDE_PROJECT_DIR is provided by Claude Code so your hook knows where the project lives even if the agent's CWD has drifted. Second, the hook fixes silently — it doesn't fail the agent's run if there's a lint warning. You want the formatter to clean up, not to surface every minor issue as an interruption.

Block Edits to Sensitive Paths

A PreToolUse hook can stop the agent from touching files it shouldn't.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/guard-paths.js"
          }
        ]
      }
    ]
  }
}

The guard script reads the tool call from stdin, checks the file path, and exits non-zero with a message if the path is in a forbidden list (.env, secrets/, infrastructure config, anything under migrations/applied/). The agent gets the error message and adjusts.

This is qualitatively different from telling the model "don't edit secrets files." The model can be talked into things. The hook can't.

Run Type Checks After Code Changes

For TypeScript or any compiled language, run the type checker after every edit.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && pnpm tsc --noEmit 2>&1 | head -50"
          }
        ]
      }
    ]
  }
}

The output goes back to the agent. If the type check fails, the agent sees the errors immediately and can fix them in the next step instead of waiting until you remind it. This shortens the "edit, break, fix" loop from minutes to seconds.

Notify on Long-Running Sessions

A Notification or Stop hook can send a desktop notification (or a message to a different terminal) when the agent finishes a task or stalls waiting for input.

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude needs input\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

On macOS, this pops a system notification. On Linux, swap to notify-send. The point is the same: you don't have to keep watching the terminal. The agent tells you when it needs you.

Inject Project Context on Session Start

A SessionStart hook can run a script that loads project-specific context into the conversation.

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cat .claude/context.md && git log --oneline -10"
          }
        ]
      }
    ]
  }
}

The agent starts every session knowing the current branch state, the last ten commits, and whatever else you want it to be aware of. This is more reliable than relying on CLAUDE.md alone, because the hook output reflects current state — not whatever was true when CLAUDE.md was last updated.

Auto-Commit Generated Test Files

Less common, but useful in test-first workflows. A PostToolUse hook can detect when the agent creates a new test file and auto-commit it under a "test" prefix.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/maybe-commit-tests.sh"
          }
        ]
      }
    ]
  }
}

The script checks whether the written file matches *.test.* or **/__tests__/**, and if so, stages and commits it. You end up with clean, small commits separating "the agent added tests" from "the agent changed implementation."

Hooks vs Prompt Engineering

The natural question: why not just tell the model what to do in the prompt or CLAUDE.md?

Sometimes that's enough. Most of the time it isn't, for a specific reason: prompt instructions compete for the model's attention with everything else in the context. The longer the conversation gets, the more likely a soft instruction gets deprioritized.

Hooks operate outside the model's reasoning. They are guarantees, not preferences. Use prompts for what the agent should do. Use hooks for the things that must always happen regardless.

For more on the prompt side of this, see Configure AI Coding Agents With Project Instructions.

The Things That Go Wrong

A few patterns that look reasonable and create pain.

Slow hooks. A PostToolUse hook that runs the full test suite after every edit will make the agent feel sluggish and burn money on token costs while the agent waits. Prefer fast, targeted checks (lint, type-check, narrow test patterns) for hooks. Save full validation for Stop or explicit prompts.

Hooks that emit noise. If your hook prints 200 lines of irrelevant output, that output goes into the agent's context window. Pipe through head, filter for actual errors, and keep hook output narrow.

Blocking hooks with poor messages. A PreToolUse hook that refuses an action should explain why in its stderr message. "Hook blocked: file matches secrets/* pattern" is useful. "exit 1" is not — the agent has nothing to work with.

Hooks that mutate things the agent can't see. A hook that writes files outside the project, modifies global state, or changes environment variables creates an invisible side effect. The agent loses track of what is true. Keep hooks scoped to operations the agent can observe.

Hooks that require interactive input. They will hang the agent. Anything in a hook must be fully non-interactive.

Sharing Hook Configurations

Hooks are most useful when they're shared across the team. Check .claude/settings.json into the repo so everyone who runs Claude Code in the project gets the same automatic behavior. Code review the hooks themselves like you'd code review any other infrastructure — a misconfigured hook can subtly break the agent loop for everyone.

The user-scoped hooks in ~/.claude/settings.json are for personal preferences (notifications, terminal-specific behavior). The project-scoped hooks are for project conventions (linting, type checks, guard rails).

Why This Matters

The bigger picture: hooks are the mechanism that turns AI coding from "interactive chat with a smart model" into a deterministic part of your development pipeline. They are the layer that makes agentic workflows reliable enough to run unattended.

You can let the agent edit code without hooks. People do it every day. But the loop where the agent edits, the linter runs, the type checker runs, the tests run, and the agent sees and fixes failures — that loop runs an order of magnitude faster than a human-in-the-loop equivalent, and it runs that fast because nobody has to remember to do any of the intermediate steps.

That speedup is where the real productivity gains live. Models are getting better. Tools are getting better. But the workflow gains from hooks are independent of either — they come from removing decisions the model would otherwise have to make.


Running multiple Claude Code sessions in parallel and want hooks to coordinate across them? Agents UI gives each agent its own persistent terminal session and project context — hooks fire in the right scope, every time.

Try Agents UI

A native terminal for AI coding agents with persistent sessions, SSH workflows, and built-in editing.