---
title: "Claude Code Hooks — Automate Workflows"
aliases: [hooks-guide, claude-hooks, lifecycle-hooks]
tags: [claude-code, hooks, automation, lifecycle, shell, workflow]
sources: [raw/Automate workflows with hooks.md]
created: 2026-04-17
updated: 2026-04-17
---

## Overview

Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. They provide **deterministic** control — certain actions always happen, regardless of what the LLM decides to do.

Four hook types exist:
- `"type": "command"` — run a shell command (most common)
- `"type": "http"` — POST event data to an HTTP endpoint
- `"type": "prompt"` — single-turn LLM evaluation (Haiku by default)
- `"type": "agent"` — multi-turn subagent with tool access (60s timeout, 50 tool turns)

---

## Hook Events Reference

| Event | When it fires |
|-------|--------------|
| `SessionStart` | Session begins or resumes |
| `UserPromptSubmit` | Prompt submitted, before Claude processes it |
| `PreToolUse` | Before a tool call — can block it |
| `PostToolUse` | After a tool call succeeds |
| `PostToolUseFailure` | After a tool call fails |
| `PermissionRequest` | Permission dialog is about to appear |
| `PermissionDenied` | Tool call denied by auto-mode classifier |
| `Notification` | Claude is waiting for input |
| `Stop` | Claude finishes responding |
| `StopFailure` | Turn ended due to API error |
| `SubagentStart` / `SubagentStop` | Subagent spawned / finished |
| `TaskCreated` / `TaskCompleted` | Task lifecycle events |
| `ConfigChange` | Config file changed during session |
| `CwdChanged` | Working directory changed |
| `FileChanged` | Watched file changed on disk |
| `PreCompact` / `PostCompact` | Before/after context compaction |
| `InstructionsLoaded` | CLAUDE.md or rules file loaded |
| `WorktreeCreate` / `WorktreeRemove` | Worktree lifecycle |
| `Elicitation` / `ElicitationResult` | MCP server requesting user input |
| `TeammateIdle` | Agent team member going idle |
| `SessionEnd` | Session terminates |

---

## Exit Codes & Output

| Exit code | Meaning |
|-----------|---------|
| `0` | Allow. stdout added to Claude's context (for `SessionStart`, `UserPromptSubmit`) |
| `2` | Block. Write reason to stderr — Claude receives it as feedback |
| Other | Allow, but log error. First line of stderr shown in transcript |

**Structured JSON output** (exit 0 + JSON to stdout) for fine-grained control:

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep"
  }
}
```

`permissionDecision` values for `PreToolUse`: `"allow"` | `"deny"` | `"ask"` | `"defer"` (non-interactive only)

> `"allow"` skips the interactive prompt but **does not override** deny rules from settings.

---

## Matchers

Without a matcher, a hook fires on every occurrence of its event. Add `"matcher"` to narrow scope.

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "prettier --write ..." }]
      }
    ]
  }
}
```

| Event | Matched field | Example values |
|-------|--------------|----------------|
| `PreToolUse`, `PostToolUse`, `PermissionRequest` | tool name | `Bash`, `Edit\|Write`, `mcp__.*` |
| `SessionStart` | start source | `startup`, `resume`, `clear`, `compact` |
| `SessionEnd` | end reason | `clear`, `resume`, `logout`, `other` |
| `ConfigChange` | config source | `user_settings`, `project_settings`, `skills` |
| `FileChanged` | literal filenames | `.envrc\|.env` |
| `SubagentStart/Stop` | agent type | `Bash`, `Explore`, `Plan` |

**`if` field** (v2.1.85+): filter by tool name + arguments, using permission rule syntax:

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

Only works on tool events (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied`).

---

## Common Recipes

### Desktop notification when Claude needs input

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

### Auto-format with Prettier after file edits

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

### Re-inject context after compaction

```json
{
  "hooks": {
    "SessionStart": [{
      "matcher": "compact",
      "hooks": [{
        "type": "command",
        "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'"
      }]
    }]
  }
}
```

### Reload direnv when directory changes

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

### Auto-approve ExitPlanMode permission

```json
{
  "hooks": {
    "PermissionRequest": [{
      "matcher": "ExitPlanMode",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
      }]
    }]
  }
}
```

---

## Hook Scopes / Configuration Files

| File | Scope | Shareable |
|------|-------|-----------|
| `~/.claude/settings.json` | All projects | No (local machine) |
| `.claude/settings.json` | Single project | Yes (commit to repo) |
| `.claude/settings.local.json` | Single project | No (gitignored) |
| Managed policy settings | Organization-wide | Yes (admin-controlled) |
| Plugin `hooks/hooks.json` | When plugin enabled | Yes |
| Skill/agent frontmatter | While active | Yes |

Disable all hooks: set `"disableAllHooks": true` in any settings file.

---

## Prompt-Based & Agent-Based Hooks

**Prompt hook** — LLM evaluates a condition and returns `{"ok": true}` or `{"ok": false, "reason": "..."}`:

```json
{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "prompt",
        "prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains\"}."
      }]
    }]
  }
}
```

**Agent hook** — spawns a subagent that can read files and run tools before deciding:

```json
{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "agent",
        "prompt": "Verify that all unit tests pass. Run the test suite. $ARGUMENTS",
        "timeout": 120
      }]
    }]
  }
}
```

Use prompt hooks when the event data alone is enough. Use agent hooks when you need to inspect actual codebase state.

---

## Troubleshooting

| Problem | Fix |
|---------|-----|
| Hook not firing | Check `/hooks` menu, verify matcher case-sensitivity, confirm correct event type |
| "hook error" in transcript | Test manually: `echo '{"tool_name":"Bash",...}' \| ./hook.sh; echo $?` |
| `/hooks` shows nothing | Validate JSON (no trailing commas/comments), check file location |
| Stop hook infinite loop | Parse `stop_hook_active` from stdin, exit 0 if `true` |
| JSON validation failed | Shell profile `echo` statements pollute stdout — wrap in `if [[ $- == *i* ]]` |
| Debug | Run `claude --debug-file /tmp/claude.log`, then `tail -f /tmp/claude.log` |

**Permission interaction:** `PreToolUse` hooks fire before permission-mode checks. A hook returning `deny` blocks even in `bypassPermissions` mode. A hook returning `allow` cannot override deny rules from settings — hooks tighten but cannot loosen.

---

## Key Takeaways

- Hooks run **deterministically** at lifecycle events — LLM cannot skip them
- Use `PreToolUse` + exit 2 to block commands; use `PostToolUse` to react after
- `matcher` filters by tool name (regex); `if` field filters by tool name + arguments together
- Multiple hooks for the same event run **in parallel**; decisions resolve to most restrictive
- `PostToolUse` cannot undo already-executed actions
- `PermissionRequest` hooks don't fire in non-interactive (`-p`) mode — use `PreToolUse` instead
- Hook stdout goes to Claude's context; stderr goes to Claude as error feedback (on exit 2)
- Three LLM-powered types: `prompt` (single call), `agent` (multi-turn with tools), `http` (external service)

---

## Related

- [[wiki/agent-sdk/overview|Agent SDK Overview]]
- [[wiki/agent-sdk/agent-skills-plugins|Agent Skills & Plugins]]
- [[wiki/agent-sdk/skills-in-sdk|Skills in the SDK]]
- [[wiki/tech-patterns/_index|Tech Patterns]]

---

## Sources

- `raw/Automate workflows with hooks.md` — clipped from https://code.claude.com/docs/en/hooks-guide
