Logo Vincent
Back to all posts

Claude Code /hooks: Make AI Follow Your Rules

Claude
Claude Code /hooks: Make AI Follow Your Rules

What Is /hooks

When using Claude Code, have you ever wished for:

  • Automatically running lint before Claude Code executes git push?
  • Sending a summary to Slack when a session ends?
  • Having another AI review the code before Claude Code deletes a file?

That’s exactly what /hooks does. It’s Claude Code’s event hook system — automatically executing your predefined logic when specific events occur.

If /permissions controls “whether something can be done,” then /hooks controls “what else happens before and after it’s done.”

Core Concepts

Events

The hook system revolves around “events.” Claude Code triggers various events during operation, and you can attach hooks to any of them.

Overview of common events:

EventWhen It FiresMatch Dimension
PreToolUseBefore tool executionTool name (e.g., Bash, Edit)
PostToolUseAfter tool executionTool name
PostToolUseFailureAfter tool execution failsTool name
UserPromptSubmitWhen user submits a message
NotificationNotification eventsNotification type
SessionStartSession beginsSource (startup/resume/clear/compact)
SessionEndSession endsEnd reason
StopClaude stops responding
SubagentStartSub-agent launchesAgent type
SubagentStopSub-agent stopsAgent type
PreCompactBefore context compaction
PostCompactAfter context compaction
PermissionRequestPermission prompt appearsTool name
PermissionDeniedPermission deniedTool name
ConfigChangeConfiguration changesConfig source
FileChangedFile changesFilename
CwdChangedWorking directory changes
InstructionsLoadedInstruction files loadedLoad reason
ElicitationMCP server asks a questionServer name
ElicitationResultMCP question resultServer name
SetupInitializationTrigger type (init/maintenance)
WorktreeCreateWorktree created
WorktreeRemoveWorktree removed
TaskCreatedTask created
TaskCompletedTask completed
TeammateIdleTeammate idle

27 events in total, covering virtually every key moment in Claude Code’s operation.

Matchers

Each event can have multiple hooks attached, using a matcher to determine when they fire.

For example, with the PreToolUse event, you can configure different hooks for different tools:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "echo 'about to run bash'" }]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "echo 'about to modify file'" }]
      }
    ]
  }
}

Three matching patterns are supported:

PatternExampleMeaning
Exact match"Bash"Matches only the Bash tool
Pipe-separated"Bash|Write|Edit"Matches any of these
Regular expression"Bash|mcp__.*"Matches Bash or any MCP tool

Leave empty or use "*" to match all cases for that event.

Four Hook Types

1. command — Shell Command

The most basic and commonly used type — directly executes a shell command:

{
  "type": "command",
  "command": "npm run lint",
  "timeout": 30000
}

Hooks receive JSON context via stdin (tool name, tool input, session ID, etc.), return results via stdout, and control behavior via exit codes:

Exit CodeMeaning
0Success — stdout content is added to conversation context
2Blocking — stderr content informs Claude, operation is blocked
OtherNon-blocking error — stderr shown as a warning

2. prompt — Single-Turn AI Judgment

Uses a lightweight AI model for single-turn evaluation — ideal for “smart review” scenarios:

{
  "type": "prompt",
  "prompt": "Check if this command might delete important files. If risky, return {\"decision\": \"block\", \"reason\": \"may delete important files\"}, otherwise return {\"decision\": \"approve\"}",
  "timeout": 30000
}

Uses a small, fast model by default (typically Haiku). You can specify a different model via the model field.

3. agent — Multi-Turn AI Agent

When single-turn evaluation isn’t enough, use the Agent type — it launches a sub-agent capable of multi-turn conversation and tool use:

{
  "type": "agent",
  "prompt": "Review this code for security issues. If problems are found, explain them in detail",
  "timeout": 60000
}

Agents can run up to 50 conversation turns with a default timeout of 60 seconds.

4. http — HTTP Request

Sends a POST request to an external service — ideal for CI/CD integration, notification systems, audit logs, etc.:

{
  "type": "http",
  "url": "https://your-webhook.example.com/hook",
  "headers": {
    "Authorization": "Bearer $API_TOKEN"
  },
  "allowedEnvVars": ["API_TOKEN"],
  "timeout": 600000
}

HTTP hooks include SSRF protection. The request body is JSON-formatted hook input. Headers support environment variable interpolation, but variables must be explicitly declared in allowedEnvVars.

How to Use /hooks

In Claude Code interactive mode, type:

/hooks

This opens a read-only browsing panel that displays all active hooks organized by event. You can:

  • Browse hierarchically: Events → Matchers → Individual hooks
  • View detailed configuration for each hook (type, command, timeout, etc.)
  • See which configuration source each hook comes from (User / Project / Local / Plugin / Session)

Note: The /hooks command itself is read-only. To add or modify hooks, edit settings.json.

Configuring Hooks in settings.json

Basic Structure

{
  "hooks": {
    "EventName": [
      {
        "matcher": "match pattern (optional)",
        "hooks": [
          {
            "type": "command | prompt | agent | http",
            "command": "...",
            "timeout": 30000
          }
        ]
      }
    ]
  }
}

Configuration Sources and Priority

Hooks can be configured at different levels, and hooks from all levels are all executed (merged, not overridden):

SourceFile Path
User~/.claude/settings.json
Project<project>/.claude/settings.json
Local<project>/.claude/settings.local.json
Plugin~/.claude/plugins/*/hooks/hooks.json
ManagedEnterprise managed policy

This differs from permission rules — permissions use “later overrides earlier,” but hooks merge and all execute.

The if Condition Filter

All hook types support an if field for more precise filtering using permission rule syntax:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'dangerous command detected'",
            "if": "Bash(rm -rf:*)"
          }
        ]
      }
    ]
  }
}

This hook only fires when the Bash tool is about to execute a command starting with rm -rf. The if syntax is the same as the rule format in /permissions.

Practical Examples

Example 1: Auto-Approve with Logging

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"decision\": \"approve\"}'"
          }
        ]
      }
    ]
  }
}

Example 2: Alert on Sensitive File Changes

{
  "hooks": {
    "FileChanged": [
      {
        "matcher": ".env|.envrc",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Warning: sensitive config file modified' | tee /dev/stderr && exit 2"
          }
        ]
      }
    ]
  }
}

When .env or .envrc files are modified, an automatic warning is triggered. Exit code 2 blocks further operations.

Example 3: AI Review for Dangerous Commands

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Analyze the command from stdin. If it could cause irreversible data loss (e.g., rm -rf, DROP TABLE), return {\"decision\": \"block\", \"reason\": \"...\"}, otherwise return {\"decision\": \"approve\"}"
          }
        ]
      }
    ]
  }
}

Example 4: Send Summary on Session End

{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "https://hooks.slack.com/services/xxx/yyy/zzz",
            "timeout": 10000
          }
        ]
      }
    ]
  }
}

Example 5: Run-Once Initialization Hook

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Session initialized at $(date)'",
            "once": true
          }
        ]
      }
    ]
  }
}

once: true means this hook runs only once per session.

Hook Input and Output

Input (stdin)

Each hook receives a JSON object via stdin containing common fields and event-specific fields:

Common fields:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/your/project",
  "permission_mode": "default"
}

Event-specific fields:

EventAdditional Fields
PreToolUsetool_name, tool_input
PostToolUsetool_name, tool_input, tool_output
UserPromptSubmituser_prompt
Notificationnotification_type, message
SessionStartsource
SessionEndreason
FileChangedfile_path, event

Output (stdout JSON)

Hooks can output structured JSON to control Claude Code’s behavior:

{
  "continue": true,
  "decision": "approve",
  "reason": "The command appears safe",
  "suppressOutput": false,
  "systemMessage": "Note: this command has been automatically reviewed"
}

Core fields:

FieldTypeDescription
continuebooleanWhether to continue execution
decisionstring"approve" or "block"
reasonstringHuman-readable explanation
suppressOutputbooleanWhether to hide hook output
systemMessagestringSystem message injected into the conversation

PreToolUse-specific output:

{
  "hookSpecificOutput": {
    "permissionDecision": "allow",
    "updatedInput": { "command": "npm test -- --coverage" },
    "additionalContext": "Automatically added coverage parameter"
  }
}

With updatedInput, you can modify the tool’s input before execution — one of the most powerful capabilities of the hook system.

Async Hooks

Some hooks don’t need to block the main flow. For notifications or logging, you don’t want Claude Code waiting for completion.

Two ways to enable async:

Method 1: Configuration Declaration

{
  "type": "command",
  "command": "curl -X POST https://example.com/log",
  "async": true,
  "asyncRewake": true
}

async: true runs the hook in the background. asyncRewake: true wakes the agent when the async hook completes to process the result.

Method 2: Runtime Declaration

The hook outputs {"async": true} as the first line of stdout to switch to async mode. This is useful when you need to decide at runtime whether to go async.

Permission Decision Priority

When multiple PreToolUse hooks make decisions about the same operation, the priority is:

deny > ask > allow

If any single hook says deny, the operation is rejected.

Important: A hook’s allow decision does NOT bypass deny/ask permission rules in settings.json. In other words, you cannot use hooks to “unlock” operations forbidden by the permission system.

Enterprise Controls

Enterprise administrators can control the hook system via managed policies:

PolicyEffect
allowManagedHooksOnly: trueOnly managed hooks execute; user and project hooks are ignored
disableAllHooks: trueAll hooks disabled (including managed ones)

When non-managed settings set disableAllHooks, only non-managed hooks are disabled — admin hooks can never be disabled by regular users.

Practical Tips

Tip 1: Use Command Hooks for Lightweight Checks

Shell command hooks start fast with minimal overhead — ideal for format checks, file existence validation, and other lightweight operations. Exit code 2 is your “emergency brake” — use it anytime to block an operation.

Tip 2: Use if to Narrow the Trigger Scope

Don’t trigger hooks for every Bash command. Use the if field for precise filtering:

{
  "type": "command",
  "command": "run-safety-check.sh",
  "if": "Bash(docker:*)"
}

This triggers the safety check only for Docker commands, avoiding unnecessary overhead.

Tip 3: Use updatedInput for Command Enhancement

PreToolUse hooks can modify tool input. For example, automatically adding --save-exact to all npm install commands:

{
  "type": "command",
  "command": "read input && echo '{\"hookSpecificOutput\": {\"updatedInput\": {\"command\": \"'$(echo $input | jq -r .tool_input.command)' --save-exact\"}}}'"
}

Tip 4: Use once to Avoid Repetition

For initialization logic that only needs to run once (environment checks, dependency installation), add once: true to prevent repeated execution on every trigger.

Tip 5: Don’t Set Timeouts Too Long

Default timeouts: command has no limit, prompt 30 seconds, agent 60 seconds, http 10 minutes. Set a reasonable timeout based on actual needs to prevent hooks from stalling and slowing down the entire session.

Final Thoughts

The hook system is the most flexible extension mechanism in Claude Code.

/permissions answers “can it be done,” while /hooks answers “what else should happen when it’s done.” Master hooks, and you can turn Claude Code into a highly automated development workflow that follows your team’s standards.

But don’t over-engineer — start with one or two simple command hooks, like “run lint before git push” or “send notification on session end.” Once you’re comfortable with the hook input/output mechanism, gradually add more complex prompt and agent hooks.

Start simple, grow as needed.

More Articles

© 2026 vincentqiao.com . All rights reserved.