Skip to content

Hooks ​

Hooks let you run shell commands at specific points in the code-assist lifecycle. They can approve or deny tool use, modify inputs and outputs, inject context, or trigger external workflows.

Hook Events ​

EventEnum ValueWhen It Fires
PreToolUsePreToolUseBefore a tool is executed (after permission check)
PostToolUsePostToolUseAfter a tool executes successfully
PostToolUseFailurePostToolUseFailureAfter a tool execution fails
UserPromptSubmitUserPromptSubmitWhen the user submits a prompt
SessionStartSessionStartWhen a new session begins
StopStopWhen the agent loop completes
NotificationNotificationWhen a notification is sent to the user
SubagentStartSubagentStartWhen a sub-agent is spawned
PermissionDeniedPermissionDeniedWhen a tool use is denied by permissions
PermissionRequestPermissionRequestWhen the user is prompted for permission
ElicitationElicitationWhen the model asks the user a question
ElicitationResultElicitationResultWhen the user answers an elicitation
FileChangedFileChangedWhen a watched file changes on disk
CwdChangedCwdChangedWhen the working directory changes
PreCompactPreCompactBefore conversation compaction
PostCompactPostCompactAfter conversation compaction

Hook Configuration ​

Hooks are defined in settings.json under the hooks key. Each hook event maps to an array of hook definitions:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "command": "python scripts/validate_tool.py",
        "timeout": 5000,
        "name": "tool-validator"
      }
    ],
    "PostToolUse": [
      {
        "command": "python scripts/log_tool_use.py",
        "timeout": 3000,
        "name": "tool-logger"
      }
    ],
    "UserPromptSubmit": [
      {
        "command": "python scripts/check_prompt.py",
        "timeout": 2000,
        "name": "prompt-guard"
      }
    ]
  }
}

Hook Execution Flow ​

Input Schema ​

Every hook receives a HookInput as JSON on stdin:

json
{
  "hook_event": "PreToolUse",
  "tool_name": "BashTool",
  "tool_input": {
    "command": "rm -rf /tmp/old-builds",
    "timeout": 120000,
    "description": "Clean old builds"
  },
  "tool_use_id": "tu_abc123",
  "tool_output": null,
  "user_prompt": null,
  "session_id": "sess_xyz789",
  "agent_id": null,
  "cwd": "/home/user/project"
}

HookInput Fields ​

FieldTypePresent For
hook_eventstrAll events
tool_namestr | nullPreToolUse, PostToolUse, PostToolUseFailure
tool_inputdict | nullPreToolUse, PostToolUse
tool_use_idstr | nullTool-related events
tool_outputstr | nullPostToolUse, PostToolUseFailure
user_promptstr | nullUserPromptSubmit
session_idstr | nullAll events
agent_idstr | nullSubagentStart
cwdstr | nullAll events

Output Schema ​

Hooks write JSON to stdout. The schema varies by event type.

PreToolUse Output ​

json
{
  "decision": "approve",
  "reason": "Command is safe",
  "updated_input": {
    "command": "rm -rf /tmp/old-builds --dry-run",
    "timeout": 120000
  }
}
FieldTypeDescription
decision"approve" | "deny" | "block"Whether to allow the tool use
reasonstr | nullExplanation for the decision
updated_inputdict | nullModified tool input (replaces original)

PostToolUse Output ​

json
{
  "suppress_output": false,
  "updated_output": "Filtered output here...",
  "additional_context": "Note: 3 files were modified"
}
FieldTypeDescription
suppress_outputbool | nullHide the tool output from the model
updated_outputstr | nullReplace the tool output
additional_contextstr | nullExtra context injected into conversation

UserPromptSubmit Output ​

json
{
  "updated_prompt": "Transformed user prompt here...",
  "prevent_continuation": false
}
FieldTypeDescription
updated_promptstr | nullReplace the user's prompt text
prevent_continuationbool | nullBlock the prompt from being sent
stop_reasonstr | nullReason for stopping (if prevent_continuation)

Universal Output Fields ​

These fields work across all hook events:

FieldTypeDescription
status_messagestr | nullProgress message shown in the UI
additional_contextstr | nullContext injected into the conversation
permission_updateslist | nullPermission rule changes to apply
retrybool | nullRetry the operation after hook completes

Hook Outcomes ​

OutcomeEnumMeaning
SuccesssuccessHook ran, output parsed successfully
BlockingblockingHook returned a blocking error — halt execution
Non-blocking errornon_blocking_errorHook failed but execution continues
CancelledcancelledHook was cancelled (timeout or abort)

Examples ​

Lint before file write ​

json
{
  "hooks": {
    "PostToolUse": [
      {
        "command": "python -c \"import sys, json; d=json.load(sys.stdin); path=d.get('tool_input',{}).get('file_path',''); import subprocess; r=subprocess.run(['ruff','check',path],capture_output=True,text=True) if path.endswith('.py') and d.get('tool_name')=='FileWriteTool' else None; json.dump({'additional_context': f'Lint results: {r.stdout}' if r else {}}, sys.stdout)\"",
        "timeout": 10000,
        "name": "auto-lint"
      }
    ]
  }
}

Block dangerous bash commands ​

python
#!/usr/bin/env python3
"""hooks/block_dangerous.py - PreToolUse hook to block dangerous commands."""
import json
import sys

BLOCKED_PATTERNS = ["rm -rf /", "sudo", "chmod 777", "> /dev/"]

data = json.load(sys.stdin)
if data.get("tool_name") == "BashTool":
    cmd = data.get("tool_input", {}).get("command", "")
    for pattern in BLOCKED_PATTERNS:
        if pattern in cmd:
            json.dump({
                "decision": "deny",
                "reason": f"Blocked: command contains '{pattern}'"
            }, sys.stdout)
            sys.exit(0)

json.dump({"decision": "approve"}, sys.stdout)
json
{
  "hooks": {
    "PreToolUse": [
      {
        "command": "python hooks/block_dangerous.py",
        "timeout": 3000,
        "name": "danger-guard"
      }
    ]
  }
}

Log all tool usage ​

python
#!/usr/bin/env python3
"""hooks/log_tools.py - PostToolUse hook that logs tool usage."""
import json
import sys
from datetime import datetime, timezone

data = json.load(sys.stdin)
log_entry = {
    "timestamp": datetime.now(timezone.utc).isoformat(),
    "event": data["hook_event"],
    "tool": data.get("tool_name"),
    "session": data.get("session_id"),
}

with open("tool_usage.jsonl", "a") as f:
    f.write(json.dumps(log_entry) + "\n")

json.dump({}, sys.stdout)

TIP

Hooks are executed sequentially in the order they appear in the configuration array. If a PreToolUse hook returns "decision": "deny", subsequent hooks for that event are still executed but their decisions are aggregated — a single deny or block takes precedence.

WARNING

Hook commands run with the same permissions as the code-assist process. Be careful with hook scripts that accept untrusted input from tool_input or user_prompt fields.

Research and educational use only. Inspired by Claude Code by Anthropic. All original rights reserved by Anthropic.