Skip to content

Hooks

Hooks let you run custom code when Claude performs specific actions. Use them to enforce policies, add notifications, or extend functionality.

Hooks are scripts that execute at specific points in Claude’s workflow.

HookTriggerUse Case
PreToolUseBefore a tool executesValidate inputs, block dangerous operations
PostToolUseAfter a tool completesAuto-format files, run linters
UserPromptSubmitWhen user submits a promptLog prompts, validate input
StopWhen Claude finishes respondingGit checks, notifications
SessionStartWhen a session beginsEnvironment setup, logging
NotificationWhen Claude sends an alertCustom notification delivery
SubagentStopWhen a subagent completesAggregate results, cleanup
PreCompactBefore message compactionSave context, log state

Your hook scripts have access to these environment variables:

VariableDescription
CLAUDE_PROJECT_DIRAbsolute path to the project root directory
CLAUDE_CODE_REMOTE"true" if running in web environment, empty if local CLI

Example usage in a hook script:

~/.claude/hooks/check-project.sh
#!/bin/bash
echo "Project directory: $CLAUDE_PROJECT_DIR"
if [ "$CLAUDE_CODE_REMOTE" = "true" ]; then
echo "Running in remote/web environment"
else
echo "Running locally"
fi

Hooks have a 60-second timeout by default. Configure per-command:

{
"hooks": {
"Stop": [{
"hooks": [{
"type": "command",
"command": "my-slow-script.sh",
"timeout": 120000
}]
}]
}
}

All matching hooks run in parallel. If you need sequential execution, combine commands in a single script.

Multiple identical hook commands are automatically deduplicated—the same command won’t run twice for the same event.

Use the /hooks command for the easiest configuration:

Terminal window
/hooks

This opens an interactive menu where you can:

  • View all configured hooks
  • Add new hooks
  • Edit existing hooks
  • Remove hooks
  • Test hook execution

Edit .claude/settings.json directly:

{
"hooks": {
"PostToolUse": [{
"matcher": "Edit",
"hooks": [{
"type": "command",
"command": "npx prettier --write $CLAUDE_PROJECT_DIR"
}]
}]
}
}

Create a settings.local.json in your project or ~/.claude/:

{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}

This plays a sound when Claude finishes a response.

Run a shell command:

{
"type": "command",
"command": "notify-send 'Claude finished'"
}

Run a script file:

{
"type": "command",
"command": "python3 ~/.claude/hooks/on-stop.py"
}

Get notified when Claude finishes long tasks:

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

Track what tools Claude uses:

{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "echo \"$(date): $TOOL_NAME\" >> ~/.claude/tool-log.txt"
}
]
}
]
}
}

Format files after Claude writes them:

{
"hooks": {
"PostToolUse": [
{
"matcher": {
"tool": "Write"
},
"hooks": [
{
"type": "command",
"command": "black $FILE_PATH 2>/dev/null || true"
}
]
}
]
}
}

Block certain commands:

~/.claude/hooks/check-command.py
#!/usr/bin/env python3
import sys
import json
import os
BLOCKED = ["rm -rf /", "DROP TABLE", "FORMAT"]
input_data = json.loads(sys.stdin.read())
command = input_data.get("command", "")
for blocked in BLOCKED:
if blocked.lower() in command.lower():
print(json.dumps({
"decision": "block",
"reason": f"Blocked dangerous pattern: {blocked}"
}))
sys.exit(0)
print(json.dumps({"decision": "allow"}))
{
"hooks": {
"PreToolUse": [
{
"matcher": {
"tool": "Bash"
},
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/check-command.py"
}
]
}
]
}
}

Filter which events trigger a hook:

{
"matcher": {
"tool": "Bash" // Only Bash commands
}
}
{
"matcher": {
"tool": "Write",
"file_pattern": "*.py" // Only Python files
}
}

Hooks can return JSON to control behavior:

{"decision": "allow"}
{
"decision": "block",
"reason": "This action is not permitted"
}
{
"decision": "allow",
"modifications": {
"command": "safer-version-of-command"
}
}
  • Directory~/.claude/
    • settings.local.json (hook configuration)
    • Directoryhooks/
      • on-stop.py
      • check-command.py
      • format-file.sh
Terminal window
# Add logging to your hook
echo "Hook triggered: $TOOL_NAME" >> /tmp/claude-hooks.log
Terminal window
# Test a command hook
echo '{"tool": "Bash", "command": "ls"}' | python3 ~/.claude/hooks/check-command.py

Force Claude to always ask “what’s next?”:

~/.claude/hooks/continue.py
#!/usr/bin/env python3
import json
print(json.dumps({
"decision": "block",
"reason": "Use the AskUserQuestion tool to ask what to do next before stopping."
}))
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/continue.py"
}
]
}
]
}
}

Now Claude will always ask for your next instruction instead of stopping.

You can add hooks directly in skill or agent frontmatter:

---
name: my-skill
hooks:
PostToolUse:
- matcher: "Write(**/*.tsx)"
command: "npm run lint:fix"
---

This keeps hooks bundled with their related functionality.