⚡ Claude Code Feature Guide

Claude Code Hooks

Run any shell command automatically when Claude edits a file, calls a tool, or finishes a session. Zero prompt engineering required.

On this page

What are Claude Code hooks?

Hooks let you wire shell commands to events inside Claude Code — the same way you'd use git hooks or npm lifecycle scripts, but for your AI coding agent.

Every time Claude performs an action (writes a file, runs a Bash command, calls a tool), hooks fire your shell code automatically. You don't change any prompts. You don't ask Claude to remember a rule. The hook runs regardless of what Claude is doing.

Real example: Claude just wrote src/auth.ts. Before you see the result, your PostToolUse hook runs prettier --write src/auth.ts && eslint --fix src/auth.ts automatically. Every write, forever, in this project.

Hooks are stored in settings.json — either project-scoped (.claude/settings.json) or user-scoped (~/.claude/settings.json). They're plain JSON, version-controllable, and shareable with your team.

The 4 hook events

Event When it fires Can block the action?
PreToolUse Before Claude calls any tool (Bash, Write, Edit, Read, …) Yes — exit non-zero to block
PostToolUse After a tool call completes successfully No — tool already ran
Stop When Claude finishes a response and stops generating No
UserPromptSubmit When the user submits a message (before Claude sees it) No

Matchers

Each hook is scoped to a tool name pattern via a matcher field. Set it to the exact tool name (Write, Bash, Edit, Read) or use "" (empty string) to match every tool call.

settings.json structure

Here is the complete JSON structure. Add a "hooks" key to either .claude/settings.json (project) or ~/.claude/settings.json (user-global):

{
  "permissions": { ... },

  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\""
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/local/bin/check-dangerous-command.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Each event key maps to an array of hook groups. Each group has a matcher (tool name or "" for all) and a hooks array. The type is always "command" — it runs the shell command string via bash -c.

Environment variables in hooks

Claude Code injects these into every hook's environment at runtime:

VariableValueAvailable in
CLAUDE_TOOL_NAMEName of the tool being called (e.g. Write)Pre + Post
CLAUDE_TOOL_INPUTFull JSON input object for the tool callPre + Post
CLAUDE_TOOL_INPUT_FILE_PATHThe file_path parameter, for Write/Edit/Read callsPre + Post
CLAUDE_TOOL_INPUT_COMMANDThe shell command string, for Bash callsPre + Post
CLAUDE_TOOL_OUTPUTTool result (stdout/return value)PostToolUse only
CLAUDE_SESSION_IDUnique ID for the current Claude Code sessionAll events
Heads up: Hook commands run with your shell's full environment plus the Claude-injected variables. They have the same filesystem access as you do. Don't add untrusted hook commands from third-party configs without reviewing them.

Copy-paste hook patterns

The most useful hooks developers add in the first week. Each is a drop-in hooks value for your settings.json.

1. Auto-format every file Claude writes

Prettier on every Write
PostToolUse Runs prettier immediately after Claude writes any file. Works with JS, TS, CSS, JSON, Markdown.
"hooks": {
  "PostToolUse": [
    {
      "matcher": "Write",
      "hooks": [
        {
          "type": "command",
          "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
        }
      ]
    }
  ]
}

2. Lint-on-write with ESLint

ESLint --fix after every Write or Edit
PostToolUse Runs ESLint with auto-fix on the modified file. The || true prevents hook failures from blocking Claude's workflow — remove it if you want strict enforcement.
"hooks": {
  "PostToolUse": [
    {
      "matcher": "Write",
      "hooks": [{ "type": "command", "command": "npx eslint --fix \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true" }]
    },
    {
      "matcher": "Edit",
      "hooks": [{ "type": "command", "command": "npx eslint --fix \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true" }]
    }
  ]
}

3. Auto-stage files Claude modifies

git add after every file write
PostToolUse Automatically stages each file Claude touches. Useful when you want to review a diff with git diff --staged at the end of a session.
"hooks": {
  "PostToolUse": [
    {
      "matcher": "Write",
      "hooks": [{ "type": "command", "command": "git add \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true" }]
    }
  ]
}

4. Desktop notification when Claude stops

Notify on Stop (macOS / Linux)
Stop Pop a desktop notification when Claude finishes a long-running task. Useful during /loop runs or slow builds.
"hooks": {
  "Stop": [
    {
      "matcher": "",
      "hooks": [
        {
          "type": "command",
          "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"' 2>/dev/null || notify-send 'Claude Code' 'Claude finished' 2>/dev/null || true"
        }
      ]
    }
  ]
}

5. Log every Bash command Claude runs

Append to an audit log
PreToolUse Writes each shell command Claude attempts to ~/.claude-audit.log with a timestamp. Good for compliance or debugging agent behavior.
"hooks": {
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "echo \"[$(date -u +%FT%TZ)] [$CLAUDE_SESSION_ID] $CLAUDE_TOOL_INPUT_COMMAND\" >> ~/.claude-audit.log"
        }
      ]
    }
  ]
}

6. Slack notification when Claude finishes

Post to Slack on Stop
Stop Sends a Slack message via incoming webhook when Claude completes a task. Replace the webhook URL with your own.
"hooks": {
  "Stop": [
    {
      "matcher": "",
      "hooks": [
        {
          "type": "command",
          "command": "curl -s -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code finished a task in session $CLAUDE_SESSION_ID\"}' YOUR_SLACK_WEBHOOK_URL"
        }
      ]
    }
  ]
}

Blocking tool calls with PreToolUse

When a PreToolUse hook exits with a non-zero status, Claude Code blocks the tool call and feeds the hook's stderr back to Claude as a rejection reason. Claude will typically explain the block to you and suggest an alternative.

Example: block writes to production config

"hooks": {
  "PreToolUse": [
    {
      "matcher": "Write",
      "hooks": [
        {
          "type": "command",
          "command": "if echo \"$CLAUDE_TOOL_INPUT_FILE_PATH\" | grep -qE '(prod|production|\.env\.prod)'; then echo 'Writing to production config is blocked by hook policy' >&2; exit 1; fi"
        }
      ]
    }
  ]
}

Example: require tests to pass before any commit

"hooks": {
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "if echo \"$CLAUDE_TOOL_INPUT_COMMAND\" | grep -q 'git commit'; then npm test --silent 2>&1 || { echo 'Tests must pass before committing' >&2; exit 1; }; fi"
        }
      ]
    }
  ]
}

Using /update-config to add hooks without editing JSON

The /update-config skill is the fastest way to add hooks — describe what you want in plain English and Claude writes the correct JSON:

  1. Open Claude Code in any project.
  2. Type: /update-config
  3. Describe the hook — for example:
    "Add a PostToolUse hook that runs prettier on every file Claude writes"
  4. Claude reads your current settings.json and patches in the correct hook JSON, handling escaping and matcher syntax for you.
Tip: You can chain multiple requests in one /update-config session: "also add an audit log hook for Bash calls" — Claude accumulates them in a single settings.json patch.

FAQ

Can a hook read the full tool input JSON, not just the file path?
Yes. $CLAUDE_TOOL_INPUT contains the complete JSON object passed to the tool. You can pipe it to jq to extract specific fields: echo "$CLAUDE_TOOL_INPUT" | jq -r '.command'.
Do hooks run for every tool, or just the matched one?
Only the matched one. A hook with "matcher": "Write" fires only when the Write tool is called — not Edit, not Bash. Set "matcher": "" to match every tool call under that event type.
Are hooks project-specific or global?
Both. .claude/settings.json in your project directory applies only to that project. ~/.claude/settings.json applies globally to every Claude Code session. Both files' hooks are merged — global hooks always fire, and project hooks layer on top.
Do hooks run asynchronously or block Claude?
They run synchronously. Claude waits for the hook to exit before proceeding. For PreToolUse hooks, this determines whether the tool runs. For PostToolUse and Stop hooks, keep them fast — a slow hook delays Claude's next action.
Can I use environment variables in hook commands?
Yes — any shell environment variable works, including the Claude-injected ones and your own shell's variables. You can also source a .env file at the start of your hook command: source .env && your-script.sh.

More about skills: see the Claude Skills Browser for the full list of slash commands that work alongside hooks.

⚡ Using Claude Code? 30 power prompts that 2× your output · £5 £3 first 10Get PDF £3 →