AI & Agents

How to Automate Your Workflow with Claude Code Hooks

Claude Code hooks are user-defined shell commands, HTTP endpoints, or LLM prompts that fire automatically at specific lifecycle events. Unlike prompt-based instructions that Claude can interpret loosely, hooks execute deterministically every time. This guide covers all five hook types, 30 lifecycle events, and practical patterns for auto-formatting, file protection, environment management, and security gates.

Fast.io Editorial Team 8 min read
Audit log interface showing automated event tracking, illustrating how Claude Code hooks log and enforce actions

What Claude Code Hooks Are and Why They Matter

Claude Code jumped from 3% to 18% developer adoption in eight months, earning a 91% customer satisfaction score in JetBrains' April 2026 survey of over 10,000 professional developers. That growth means millions of developers now rely on Claude Code to edit files, run shell commands, and scaffold projects. But most of them control the tool's behavior through prompt instructions alone, which Claude can interpret loosely or skip entirely. Hooks close that gap.

Claude Code hooks are user-defined actions that execute automatically at specific points in the tool's lifecycle. They are deterministic: a hook fires every time its trigger event occurs, regardless of how Claude interprets your prompt. Where a CLAUDE.md instruction says "please run Prettier after editing files," a PostToolUse hook runs Prettier after every edit, with zero exceptions.

The hooks system supports five handler types:

  • Command hooks run shell scripts that receive event data as JSON on stdin and return decisions via exit codes and stdout
  • HTTP hooks POST event data to a URL and read the response for decisions, useful for shared team services or cloud functions
  • MCP tool hooks call tools on connected MCP servers, letting you chain hooks into existing tool ecosystems
  • Prompt hooks send event context to a Claude model for single-turn yes/no evaluation
  • Agent hooks spawn a subagent with tool access for multi-step verification (experimental)

Each hook attaches to one of 30 lifecycle events, from SessionStart (when you open Claude Code) to SessionEnd (when you close it), with granular tool-level events like PreToolUse and PostToolUse in between. Matchers filter which specific tool calls or event types trigger the hook, so you target exactly the behavior you want to control.

How to Configure Hooks in settings.json

Hooks live in JSON settings files. The file you choose determines who the hook affects and whether it ships with your project:

  • ~/.claude/settings.json: All your projects, local to your machine
  • .claude/settings.json: Single project, committable to the repo
  • .claude/settings.local.json: Single project, gitignored
  • Managed policy settings: Organization-wide, admin-controlled

Project-level hooks (.claude/settings.json) are the most common starting point. They travel with the repo, so every developer on the team gets the same guardrails.

Here is a complete first hook that sends a macOS desktop notification whenever Claude finishes and needs your input:

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

The structure nests three levels: the event name (Notification), a matcher group (empty string matches all notification types), and the hook handlers. After saving the file, type /hooks in Claude Code to verify the hook appears under the Notification event.

Matchers and the if field

Matchers filter at the event level. For tool events like PreToolUse or PostToolUse, the matcher checks the tool name. "Edit|Write" fires only on file-editing tools. "Bash" fires only on shell commands. "mcp__github__.*" uses a regex to match any tool from the GitHub MCP server.

For finer control, the if field filters by tool arguments using permission rule syntax:

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "if": "Bash(git *)",
      "command": ".claude/hooks/check-git-policy.sh"
    }
  ]
}

This hook fires only when Claude runs a git subcommand, not on every Bash command. The parser inspects compound commands too: npm test && git push matches because git push is a recognized subcommand within the pipeline.

Task list interface showing automated workflow steps

Hook Lifecycle Events and Exit Codes

Claude Code exposes 30 lifecycle events organized into four groups.

Session events control startup and teardown. SessionStart fires when a session begins or resumes, with matchers for startup, resume, clear, or compact. SessionEnd fires on termination. Setup fires during CI initialization with --init-only or --maintenance.

Tool events are the workhorses. PreToolUse fires before a tool call executes and can block it. PostToolUse fires after success, PostToolUseFailure after failure. PostToolBatch fires after a group of parallel tool calls resolves. PermissionRequest fires when the permission dialog appears, giving hooks a chance to auto-approve or deny specific tools.

Turn events bracket Claude's responses. UserPromptSubmit fires before Claude processes your input. Stop fires when Claude finishes responding. StopFailure fires on API errors. MessageDisplay fires while the response streams, useful for real-time monitoring.

Environment events react to external changes. CwdChanged fires when the working directory changes. FileChanged watches specific files you define in the matcher. ConfigChange fires when a settings or skills file is modified. WorktreeCreate and WorktreeRemove fire during git worktree operations.

Exit codes and blocking

Your hook communicates decisions through exit codes:

  • Exit 0: No objection. The normal permission flow continues.
  • Exit 2: Block the action. Write a reason to stderr, and Claude receives it as feedback so it can adjust its approach.
  • Any other code: Non-blocking error. The action proceeds, and a warning appears in the transcript.

For structured decisions beyond blocking, exit 0 and print a JSON object to stdout. A PreToolUse hook can return "permissionDecision": "deny" with a reason, or "allow" to skip the permission prompt. When multiple hooks fire on the same event, the most restrictive decision wins: deny beats ask, ask beats allow.

Fastio features

Give your Claude Code hooks a persistent workspace

Shared storage and MCP-ready endpoints for syncing agent output, logging audit trails, and sharing hook-automated results with your team. Starts with a 14-day free trial.

Practical Hook Patterns

Auto-format code after edits

This PostToolUse hook runs Prettier on every file Claude writes or modifies:

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

The command reads the edited file path from JSON input using jq and passes it to Prettier. Because this runs after every file-editing tool call, formatting stays consistent without relying on Claude to remember your project's style rules.

Block edits to protected files

Save this script to .claude/hooks/protect-files.sh and make it executable with chmod +x:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done
exit 0

Register it under PreToolUse with an Edit|Write matcher. Exit code 2 blocks the edit and sends the stderr message back to Claude, which adjusts its approach and tries a different path.

Reload environment on directory change

If your project uses direnv or devbox, this pair of hooks keeps Claude's Bash environment in sync:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
          }
        ]
      }
    ],
    "CwdChanged": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
          }
        ]
      }
    ]
  }
}

CLAUDE_ENV_FILE points to a file that Claude Code sources before each Bash command. Writing direnv's exports there keeps environment variables current as Claude navigates between directories. The same pattern works with devbox shellenv if you use devbox instead.

Re-inject context after compaction When Claude's context window fills up, compaction summarizes the conversation and can lose important details. A SessionStart hook with a compact matcher re-injects critical project reminders:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use pnpm, not npm. Run tests before committing. Current sprint: auth refactor.'"
          }
        ]
      }
    ]
  }
}

Anything your command writes to stdout is added to Claude's context after compaction. You could replace the echo with git log --oneline -5 to remind Claude of recent commits, or cat .claude/project-rules.txt to reload a full ruleset.

Audit configuration changes

Track every settings or skills file change for compliance:

{
  "hooks": {
    "ConfigChange": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
          }
        ]
      }
    ]
  }
}

The ConfigChange event fires when an external process or editor modifies a configuration file. To block a change from taking effect, exit with code 2 or return {"decision": "block"} in the JSON output.

Audit trail interface showing automated tracking of actions and changes

HTTP, Prompt, and Agent Hooks

Command hooks cover most automation needs, but three other hook types unlock patterns that shell scripts cannot handle alone.

HTTP hooks for team-wide services

HTTP hooks POST event data to a URL instead of running a local command:

{
  "type": "http",
  "url": "http://localhost:8080/hooks/tool-use",
  "headers": {
    "Authorization": "Bearer $MY_TOKEN"
  },
  "allowedEnvVars": ["MY_TOKEN"]
}

The endpoint receives the same JSON a command hook would get on stdin. To block an action, return a 2xx response with hookSpecificOutput fields. Header values support environment variable interpolation, but only variables listed in allowedEnvVars are resolved.

HTTP hooks let a team run a single policy server that every developer's Claude Code instance reports to. Centralized audit logging, compliance checks, or integration with workflow tools like Fast.io workspaces all become straightforward. A PostToolUse HTTP hook could sync every edited file to a shared workspace via the Fast.io MCP server, giving your team a live audit trail without distributing scripts.

Prompt hooks for judgment-based decisions

Prompt hooks replace shell regex with LLM judgment. Instead of enumerating every dangerous command pattern, you describe what to look for:

{
  "type": "prompt",
  "prompt": "Check if all requested tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains\"}."
}

The model (Haiku by default, configurable with the model field) receives your prompt plus the event's input data and returns {"ok": true} or {"ok": false, "reason": "..."}. For Stop hooks, a false result sends the reason back to Claude as its next instruction, keeping it working until the condition is met.

A PreToolUse prompt hook can evaluate whether a Bash command looks safe, catching patterns that are hard to express as regex. This is where hooks cross from deterministic automation into judgment-based security gates, a pattern that no existing guides cover in depth.

Agent hooks for multi-step verification Agent hooks spawn a subagent with tool access. Unlike prompt hooks that make a single LLM call, agent hooks can read files, search code, and run commands before deciding:

{
  "type": "agent",
  "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
  "timeout": 120
}

Agent hooks are experimental and have a default timeout of 60 seconds with up to 50 tool-use turns. They are the right choice when the decision depends on the actual state of the codebase, not just the event data passed to the hook.

Combining hooks with MCP tooling

For teams that use AI agents alongside human collaborators, hooks connect the automation pipeline to shared infrastructure. Fast.io's MCP server exposes 19 tools for workspace, storage, AI, and workflow operations. A PostToolUse hook can call Fast.io's MCP tools to sync files after every edit, or a SessionEnd hook can upload audit logs to a shared workspace. Every org starts with a 14-day free trial, with storage, monthly credits, and workspaces scaled to each plan, enough to run hooks-driven automation pipelines from day one.

Frequently Asked Questions

What are Claude Code hooks?

Claude Code hooks are user-defined shell commands, HTTP endpoints, or LLM prompts that execute automatically at specific lifecycle events in Claude Code. They provide deterministic control over behavior that would otherwise depend on how the model interprets prompt instructions. Hooks can auto-format code, block dangerous commands, send notifications, enforce project rules, and integrate with external services. They are configured in settings.json files at the user, project, or organization level.

How do I create a hook in Claude Code?

Add a hooks block to your settings.json file (either ~/.claude/settings.json for all projects or .claude/settings.json for a single project). Each hook specifies an event name, an optional matcher to filter when it fires, and one or more handler definitions. After saving, type /hooks in Claude Code to verify the hook is registered. You can also ask Claude directly to write a hook by describing what you want.

Can Claude Code hooks run HTTP requests?

Yes. Set the hook type to "http" and provide a URL. Claude Code POSTs the event data as JSON to that endpoint and reads the response for decisions. You can include authorization headers with environment variable interpolation. HTTP hooks are useful for centralized policy servers, audit logging services, or integration with external workflow tools that a whole team shares.

What events can trigger Claude Code hooks?

Claude Code exposes 30 lifecycle events. The most commonly used are PreToolUse (before a tool executes, can block it), PostToolUse (after a tool succeeds), Stop (when Claude finishes responding), SessionStart (session begins or resumes), Notification (Claude needs input), CwdChanged (directory changes), and ConfigChange (settings file modified). Each event provides specific JSON data to your hook and supports different decision types.

What is the difference between prompt hooks and agent hooks?

Prompt hooks make a single LLM call. They send the event data plus your prompt to a Claude model (Haiku by default) and get back a yes/no decision. Agent hooks spawn a full subagent that can read files, search code, run commands, and use up to 50 tool turns before deciding. Use prompt hooks when the event data alone is enough to judge. Use agent hooks when you need to verify something against the actual codebase state, such as confirming that tests pass before allowing Claude to stop.

Can hooks override Claude Code permissions?

Hooks can tighten restrictions but not loosen them. A PreToolUse hook returning "deny" blocks a tool call even in bypass-permissions mode. But a hook returning "allow" only skips the interactive permission prompt. If a deny rule in your settings matches the tool call, the deny rule still wins. This design ensures that enterprise-managed deny lists cannot be overridden by project-level hooks.

How do I debug hooks that are not firing?

Start with /hooks in Claude Code to confirm the hook appears under the correct event. Check that your matcher pattern matches the tool name exactly (matchers are case-sensitive). Verify you are triggering the right event type, since PreToolUse fires before execution while PostToolUse fires after. For detailed execution logs, start Claude Code with the --debug-file flag and tail the log file in a separate terminal. If your hook's JSON output fails to parse, check that your shell profile does not print text in non-interactive mode, which can prepend garbage to the JSON.

Related Resources

Fastio features

Give your Claude Code hooks a persistent workspace

Shared storage and MCP-ready endpoints for syncing agent output, logging audit trails, and sharing hook-automated results with your team. Starts with a 14-day free trial.