Skip to content

Hooks

Event-driven automation that fires deterministically on specific agent events.

Hooks are event-driven automation that fires on specific agent events. Unlike AGENTS.md instructions (which are non-deterministic), hooks execute deterministically — they run 100% of the time when triggered.

How hooks work

Hooks let you run commands, call HTTP endpoints, or invoke AI evaluations automatically in response to agent events. Use them for linting, formatting, security checks, logging, and anything else that should happen consistently without relying on the agent to remember.

Key behaviors:

  • All matching hooks for an event run in parallel
  • Identical handlers are deduplicated automatically (by command string or URL)
  • Hooks run in the current directory with the agent’s environment
  • Settings file changes are picked up automatically by a file watcher

Configuration format

Hooks are configured in settings JSON files under the hooks key. Each event maps to an array of matcher groups, and each matcher group contains an array of hook handlers.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/my-script.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Structure breakdown

hooks
  └── EventName (e.g., "PreToolUse")
        └── Array of matcher groups
              ├── matcher: regex to filter when this group activates
              └── hooks: array of hook handlers
                    ├── type: "command" | "http" | "prompt" | "agent"
                    ├── if: permission rule filter (tool events only)
                    ├── timeout: seconds before timeout
                    ├── statusMessage: custom spinner text
                    ├── once: run only once per session (boolean)
                    └── [type-specific fields]

Event reference

Blocking events

These events pause execution until hooks complete. Hooks can block or modify the operation.

EventFires whenMatcher matches against
PreToolUseBefore the agent runs a toolTool name (Bash, Edit, mcp__.*)
PermissionRequestWhen the agent needs permissionTool name
UserPromptSubmitWhen you submit a promptNo matcher support
StopWhen the agent finishes respondingNo matcher support
SubagentStopWhen a subagent finishesAgent type (Explore, Bash, Plan)
TaskCreatedWhen a task is createdNo matcher support
TaskCompletedWhen a task completesNo matcher support
TeammateIdleWhen a teammate goes idleNo matcher support
ConfigChangeWhen settings changeConfig source (project_settings, etc.)
ElicitationWhen MCP server shows a formMCP server name
ElicitationResultWhen form is submittedMCP server name
WorktreeCreateWhen creating a worktreeNo matcher support

Non-blocking events

These events fire-and-forget. Hooks run but do not pause the agent’s execution.

EventFires whenMatcher matches against
PostToolUseAfter the agent runs a toolTool name
PostToolUseFailureAfter a tool failsTool name
PermissionDeniedWhen auto mode denies a tool callTool name
NotificationWhen the agent sends a notificationNotification type (permission_prompt, idle_prompt)
SubagentStartWhen a subagent startsAgent type
SessionStartWhen a session beginsSession source (startup, resume, clear)
SessionEndWhen a session endsEnd reason (clear, resume, logout)
StopFailureWhen the agent stops due to errorError type (rate_limit, billing_error, etc.)
CwdChangedWhen working directory changesNo matcher support
FileChangedWhen a watched file changesFilename (basename)
PreCompactBefore context compactionTrigger (manual, auto)
PostCompactAfter context compactionTrigger (manual, auto)
InstructionsLoadedWhen an AGENTS.md is loadedLoad reason (session_start, include)
WorktreeRemoveWhen a worktree is removedNo matcher support

Hook types

Command hooks

Execute shell commands. The most common type. Receive event data as JSON on stdin and communicate results via exit codes and stdout.

{
  "type": "command",
  "command": "./scripts/lint-check.sh",
  "async": false,
  "timeout": 600
}
  • command (required): Shell command to run
  • async (optional): Run in background without blocking (default: false)
  • shell (optional): "bash" or "powershell" (default: "bash")
  • Default timeout: 600 seconds

HTTP hooks

Send a POST request with event data as JSON body.

{
  "type": "http",
  "url": "http://localhost:8080/hooks/pre-tool-use",
  "headers": {
    "Authorization": "Bearer $MY_TOKEN"
  },
  "allowedEnvVars": ["MY_TOKEN"],
  "timeout": 30
}
  • url (required): Endpoint URL
  • headers (optional): HTTP headers (supports $VAR interpolation)
  • allowedEnvVars (optional): Env vars that can be interpolated into headers
  • Default timeout: 30 seconds

Prompt hooks

Send a single-turn prompt to the model for yes/no decisions.

{
  "type": "prompt",
  "prompt": "Does this command look safe? $ARGUMENTS",
  "model": "fast",
  "timeout": 30
}
  • prompt (required): Evaluation text ($ARGUMENTS is replaced with event data)
  • model (optional): Model alias to use
  • Default timeout: 30 seconds

Agent hooks

Spawn a subagent with tool access to verify conditions.

{
  "type": "agent",
  "prompt": "Review this file change for security issues. $ARGUMENTS",
  "model": "standard",
  "timeout": 60
}
  • prompt (required): Task description for the agent
  • model (optional): Model alias to use
  • Default timeout: 60 seconds

Matchers and the if field

Matchers

The matcher field is a regex that determines when a matcher group activates. Use "*", "", or omit entirely to match all events of that type.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "./lint.sh" }]
      }
    ]
  }
}

For MCP tools, the naming pattern is mcp__<server>__<tool>:

  • mcp__memory__.* matches all memory server tools
  • mcp__.*__write.* matches write tools from any server

The if field

The if field provides additional filtering using permission rule syntax. It only works on tool events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest).

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "if": "Bash(rm *)",
      "command": "./scripts/block-rm.sh"
    }
  ]
}

This hook only fires for Bash tool calls whose command matches rm *. The if field prevents the hook process from spawning at all when the pattern does not match.

Command hook input and output

Input (stdin)

Command hooks receive JSON on stdin with these common fields:

{
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite"
  }
}

The exact fields vary by event. Tool events include tool_name and tool_input. PostToolUse adds tool_response. Stop includes last_assistant_message.

Output (exit codes)

Exit codeBehavior
0Success. JSON output parsed if present
2Blocking error. Blocks the operation (tool call denied, prompt rejected, etc.)
OtherNon-blocking error. stderr shown in verbose mode; execution continues

JSON output (exit 0)

{
  "continue": true,
  "stopReason": "optional message when continue is false",
  "suppressOutput": false,
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask",
    "permissionDecisionReason": "Reason shown to user"
  }
}

Set "continue": false to stop the agent immediately. The hookSpecificOutput fields vary by event — for example, PreToolUse supports permissionDecision and updatedInput, while PostToolUse supports additionalContext.

Practical examples

Auto-lint after file edits

Run your linter every time the agent writes or edits a file:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx eslint --fix $(cat | jq -r '.tool_input.file_path')",
            "statusMessage": "Running linter..."
          }
        ]
      }
    ]
  }
}

Block destructive commands

Prevent the agent from running rm -rf without your approval:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": ".agent/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

The hook script (.agent/hooks/block-rm.sh):

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "rm -rf blocked by hook"
    }
  }'
else
  exit 0
fi

Add context to every prompt

Inject environment information into the agent’s context on every prompt:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"UserPromptSubmit\",\"additionalContext\":\"Current branch: '$(git branch --show-current)'\"}}'",
            "statusMessage": "Loading context..."
          }
        ]
      }
    ]
  }
}

Log all tool usage via HTTP

Send tool usage to a monitoring endpoint:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "http",
            "url": "http://localhost:8080/hooks/tool-usage",
            "headers": {
              "Authorization": "Bearer $HOOK_TOKEN"
            },
            "allowedEnvVars": ["HOOK_TOKEN"]
          }
        ]
      }
    ]
  }
}

Prevent the agent from stopping prematurely

Force the agent to keep going when it tries to stop without running tests:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Review the agent's last message. Did it run the test suite before stopping? If tests were not run and code was changed, respond NO.",
            "statusMessage": "Checking if tests were run..."
          }
        ]
      }
    ]
  }
}

Environment variables

These environment variables are available to all command hooks:

VariableDescription
AGENT_PROJECT_DIRProject root directory
AGENT_CODE_REMOTE"true" in remote/web environments; unset locally

SessionStart, CwdChanged, and FileChanged hooks also receive:

VariableDescription
AGENT_ENV_FILEPath to a file where you can persist env vars using export statements

Settings file locations

FileScopeShared via git?
~/.agent/settings.jsonAll projects (personal)No
.agent/settings.jsonThis project (team)Yes
.agent/settings.local.jsonThis project (personal)No (gitignored)
Managed policy settingsOrganization-wideAdmin-controlled

Team hooks in .agent/settings.json apply to everyone who clones the repo. Personal hooks in .agent/settings.local.json add to (but cannot remove) team hooks.

To disable all hooks, set "disableAllHooks": true in your settings file. Note that managed policy hooks cannot be disabled by user or project settings.

Browsing configured hooks

Use the /hooks slash command inside the agent to browse all configured hooks. It shows every event with a count of configured hooks, lets you drill into matchers and view handler details, and labels each hook with its source: [User], [Project], [Local], [Plugin], [Session], or [Built-in]. The /hooks view is read-only.

Hooks vs. AGENTS.md instructions

AspectHooksAGENTS.md
ExecutionDeterministic (always runs)Non-deterministic (agent may skip)
ScopeShell commands, HTTP calls, AI evaluationNatural language instructions
Best forFormatting, linting, security gates, loggingCoding conventions, style preferences
Failure modeBlocks the operationAgent might not follow it

Rule of thumb: If something must always happen, use a hook. If it is a preference or guideline, use AGENTS.md.

Quick reference

What you wantEventMatcherHook type
Lint after file changesPostToolUseEdit|Writecommand
Block dangerous commandsPreToolUseBash + if fieldcommand
Add context to promptsUserPromptSubmit(none)command
Custom notificationsNotificationnotification typecommand or http
Validate before stoppingStop(none)prompt or command
Log tool usagePostToolUse"" (all tools)http
Monitor MCP toolsPreToolUsemcp__server__.*command
Set up session environmentSessionStart(none)command
React to config changesConfigChangeconfig sourcecommand

Tips

  • Start with a PostToolUse lint hook on Edit|Write — it has the highest payoff-to-effort ratio.
  • Keep hook commands fast. Slow hooks disrupt the interactive workflow.
  • Use "async": true on command hooks for non-critical operations like logging.
  • Test hooks manually before adding them to settings — a broken hook on a blocking event will block all operations of that type.
  • Use the if field to narrow tool-event hooks without spawning a process for every invocation.
  • Use the statusMessage field to show a custom spinner message while your hook runs.
  • Set "once": true for hooks that only need to run once per session (e.g., environment setup).
  • Combine hooks with permissions: hooks enforce standards deterministically, permissions control what the agent can do interactively.
  • Use /hooks to verify your hooks are loaded and sourced correctly.