Skip to content

Pre/Post Tool Hooks

Hooks let you run shell commands automatically when Claude Code performs certain actions. They are useful for enforcing code quality, triggering builds, sending notifications, and integrating with your existing development workflow.

What Are Hooks?

A hook is a shell command that executes in response to a Claude Code event. For example, you can run a linter every time Claude edits a file, or send a notification when a task completes. Hooks run synchronously — Claude Code waits for them to finish before continuing.

Hooks provide feedback directly to Claude. If a hook exits with a non-zero status, Claude sees the failure and can adjust its approach.

Configuring Hooks

Hooks are defined in claude_internal/settings.json (project-level) or ~/claude_internal/settings.json (global):

json { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "command": "eslint --fix \"$CLAUDE_FILE_PATH\"", "description": "Auto-fix lint issues after file edits" } ], "Notification": [ { "matcher": "", "command": "notify-send 'Claude Code' \"$CLAUDE_NOTIFICATION\"", "description": "Desktop notification on completion" } ] } }

Each hook specifies:

  • matcher — A regex pattern to filter which tools or events trigger the hook. An empty string matches everything.
  • command — The shell command to execute. Environment variables provide context about the event.
  • description — A human-readable explanation shown in logs.

Hook Events

PreToolUse

Runs before a tool executes. The hook can block the tool by exiting with a non-zero status. Useful for validation and safety checks.

Available variables: CLAUDE_TOOL_NAME, CLAUDE_FILE_PATH (for file operations), CLAUDE_COMMAND (for Bash).

PostToolUse

Runs after a tool completes successfully. Common uses include formatting, linting, and logging.

Available variables: same as PreToolUse, plus CLAUDE_TOOL_RESULT with the tool output.

Notification

Runs when Claude Code sends a notification, such as when it finishes a task and is waiting for input.

Available variables: CLAUDE_NOTIFICATION with the notification message.

PreUserPromptSubmit

Runs before a user prompt is processed. Can be used to inject context or validate prompts.

Example Hooks

Auto-format on Save

Run Prettier after every file edit:

json { "matcher": "Edit|Write", "command": "prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true", "description": "Format files after editing" }

Lint Check Before Commit

Validate changes before Claude runs git commit:

json { "matcher": "Bash", "command": "if echo \"$CLAUDE_COMMAND\" | grep -q 'git commit'; then npm run lint; fi", "description": "Run linter before git commits" }

Sound Notification on Completion

Play a sound when Claude finishes and is waiting:

json { "matcher": "", "command": "powershell -c '[console]::beep(600,300)'", "description": "Beep when Claude needs attention" }

Log All Tool Usage

Keep an audit trail of what Claude does:

json { "matcher": "", "command": "echo \"$(date): $CLAUDE_TOOL_NAME\" >> ~/claude_internal/tool-log.txt", "description": "Log tool usage to file" }

Writing Hook Scripts

For complex logic, put your hook in a script file rather than inlining it:

json { "matcher": "Edit|Write", "command": "bash claude_internal/hooks/post-edit.sh", "description": "Run post-edit checks" }

In claude_internal/hooks/post-edit.sh:

```bash

!/bin/bash

FILE="$CLAUDE_FILE_PATH" EXT="${FILE##*.}"

case "$EXT" in ts|tsx) npx eslint --fix "$FILE" ;; py) ruff check --fix "$FILE" ;; rs) rustfmt "$FILE" ;; esac ```

More Hook Recipes

Auto-Run Tests After Source File Edits

Run the related test file whenever Claude edits a source file:

json { "matcher": "Edit|Write", "command": "bash claude_internal/hooks/auto-test.sh \"$CLAUDE_FILE_PATH\"", "description": "Run related tests after file edits" }

```bash

!/bin/bash

claude_internal/hooks/auto-test.sh

FILE="$1"

Skip if editing a test file itself

echo "$FILE" | grep -qE '.(test|spec).' && exit 0

Skip non-source files

echo "$FILE" | grep -qE '.(ts|tsx|js|jsx)$' || exit 0

Derive test path and run if it exists

TEST=$(echo "$FILE" | sed 's/.tsx\?$/.test&/') [ -f "$TEST" ] && npx jest "$TEST" --no-coverage 2>&1 | tail -10 exit 0 ```

Type Check After TypeScript Edits

json { "matcher": "Edit|Write", "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.tsx?$'; then npx tsc --noEmit 2>&1 | head -20; fi", "description": "Type check after TypeScript edits" }

Python Lint and Format Pipeline

json { "matcher": "Edit|Write", "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -q '\\.py$'; then ruff format \"$CLAUDE_FILE_PATH\" && ruff check --fix \"$CLAUDE_FILE_PATH\"; fi", "description": "Format and lint Python files after edits" }

Go Vet and Format

json { "matcher": "Edit|Write", "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -q '\\.go$'; then gofmt -w \"$CLAUDE_FILE_PATH\" && go vet ./... 2>&1 | head -10; fi", "description": "Format and vet Go files after edits" }

Slack Notification on Task Completion

Send a Slack message when Claude finishes a task:

json { "matcher": "", "command": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"text\":\"Claude Code: '\"$CLAUDE_NOTIFICATION\"'\"}' \"$SLACK_WEBHOOK_URL\" > /dev/null", "description": "Send Slack notification on completion" }

Set SLACK_WEBHOOK_URL in your environment or .env file.

Block File Edits Outside Project

Prevent Claude from editing files outside the project root:

json { "matcher": "Edit|Write", "command": "echo \"$CLAUDE_FILE_PATH\" | grep -q '^/' && ! echo \"$CLAUDE_FILE_PATH\" | grep -q \"$(pwd)\" && echo 'BLOCKED: edit outside project root' >&2 && exit 1 || true", "description": "Block edits outside project directory" }

Migration Safety Check

Prevent Claude from running database migrations without confirmation:

json { "matcher": "Bash", "command": "if echo \"$CLAUDE_COMMAND\" | grep -qE 'migrate|migration'; then echo 'WARNING: Migration detected. Verify the migration file before proceeding.' >&2; fi", "description": "Warn on database migration commands" }

Tips and Caveats

  • Keep hooks fast. Claude waits for hooks to complete. Long-running hooks slow down the entire workflow.
  • Handle errors gracefully. Use || true if a hook failure should not block Claude.
  • Test hooks manually. Run the command with sample environment variables before adding it to settings.
  • Use project-level hooks for team-specific workflows and global hooks for personal preferences.
  • Hooks see the same environment as Claude Code, including your PATH and installed tools.
  • PreToolUse hooks can block actions. A non-zero exit prevents the tool from running. Use this carefully.

See Also