Skip to main content
Hooks let you execute shell commands at key points in an agent’s lifecycle. They provide deterministic control that works alongside the LLM’s behavior — useful for validation, logging, environment setup, notifications, and blocking dangerous operations.
Common use cases:
  • Validate or transform tool inputs before execution
  • Block dangerous operations based on custom rules
  • Log all tool calls to an audit file
  • Set up the environment when a session starts
  • Send external notifications when errors occur
  • Log model responses for analytics or compliance

Hook events

There are seven hook events:
EventWhen it firesCan block?
pre_tool_useBefore a tool call executesYes
post_tool_useAfter a tool completes successfullyNo
session_startWhen a session begins or resumesNo
session_endWhen a session terminatesNo
on_user_inputWhen the agent is waiting for user inputNo
stopWhen the model finishes respondingNo
notificationWhen the agent emits a notification (error/warning)No

Configuration

Hooks are configured under agents.<name>.hooks:
agents:
  root:
    model: openai/gpt-4o
    description: Agent with hooks
    instruction: You are a helpful assistant.
    hooks:
      pre_tool_use:
        - matcher: "shell|edit_file"
          hooks:
            - type: command
              command: "./scripts/validate.sh"
              timeout: 30

      post_tool_use:
        - matcher: "*"
          hooks:
            - type: command
              command: "./scripts/log.sh"

      session_start:
        - type: command
          command: "./scripts/setup.sh"

      session_end:
        - type: command
          command: "./scripts/cleanup.sh"

      on_user_input:
        - type: command
          command: "./scripts/notify.sh"

      stop:
        - type: command
          command: "./scripts/log-response.sh"

      notification:
        - type: command
          command: "./scripts/alert.sh"

Hook definition fields

type
string
required
Hook type. Currently only command is supported.
command
string
required
Shell command or inline shell script to execute. Multi-line scripts are supported using YAML block scalars (|).
timeout
number
default:"60"
Execution timeout in seconds. The hook is killed if it exceeds this limit.

Matcher patterns

pre_tool_use and post_tool_use hooks use a matcher field with regex patterns to match tool names:
PatternMatches
*All tools
shellOnly the shell tool
shell|edit_fileEither shell or edit_file
mcp:.*All MCP tools

Hook input

Hooks receive JSON on stdin with context about the event:
{
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "hook_event_name": "pre_tool_use",
  "tool_name": "shell",
  "tool_use_id": "call_xyz",
  "tool_input": {
    "cmd": "rm -rf /tmp/cache"
  }
}

Fields by event type

Fieldpre_tool_usepost_tool_usesession_startsession_endon_user_inputstopnotification
session_id
cwd
hook_event_name
tool_name
tool_use_id
tool_input
tool_response
source
reason
stop_response
notification_level
notification_message
Notes:
  • source for session_start: startup, resume, clear, or compact
  • reason for session_end: clear, logout, prompt_input_exit, or other
  • notification_level: error or warning

Hook output

Hooks communicate back by writing JSON to stdout:
{
  "continue": true,
  "stop_reason": "Optional message when continue=false",
  "suppress_output": false,
  "system_message": "Warning shown to user"
}
FieldTypeDescription
continuebooleanWhether to continue execution. Default: true.
stop_reasonstringMessage displayed to the user when continue is false.
suppress_outputbooleanWhen true, hides the hook’s stdout from the transcript.
system_messagestringWarning message displayed to the user.

Pre-tool-use output

pre_tool_use hooks support additional output via hook_specific_output:
{
  "hook_specific_output": {
    "hook_event_name": "pre_tool_use",
    "permission_decision": "deny",
    "permission_decision_reason": "Dangerous command blocked",
    "updated_input": { "cmd": "modified command" }
  }
}
FieldValuesDescription
permission_decisionallow, deny, askOverride the permission decision for this call
permission_decision_reasonstringExplanation shown to the user
updated_inputobjectReplacement tool input (replaces original)

Plain text output

For session_start, post_tool_use, and stop hooks, plain text written to stdout (non-JSON) is captured and injected as additional context for the agent.

Exit codes

Exit codeMeaning
0Success — continue normally
2Blocking error — stop the operation
OtherError — logged, execution continues
Hooks run synchronously and add latency to every matched operation. Keep hook scripts fast. Use suppress_output: true for logging-only hooks.
session_end hooks run even when the session is interrupted (e.g., Ctrl+C), subject to their configured timeout.

Examples

Block dangerous commands

hooks:
  pre_tool_use:
    - matcher: "shell"
      hooks:
        - type: command
          timeout: 10
          command: |
            INPUT=$(cat)
            CMD=$(echo "$INPUT" | jq -r '.tool_input.cmd // ""')

            if echo "$CMD" | grep -qiE 'rm\s+.*-rf|sudo|mkfs'; then
              cat <<'EOF'
            {"hook_specific_output":{"permission_decision":"deny","permission_decision_reason":"Dangerous command blocked by policy"}}
            EOF
              exit 2
            fi

            echo '{"continue": true}'

Audit logging

hooks:
  post_tool_use:
    - matcher: "*"
      hooks:
        - type: command
          command: |
            INPUT=$(cat)
            TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
            TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
            SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
            echo "$TIMESTAMP | $SESSION_ID | $TOOL_NAME" >> ./audit.log
            echo '{"continue": true}'

Session lifecycle

hooks:
  session_start:
    - type: command
      timeout: 10
      command: |
        INPUT=$(cat)
        SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
        echo "Session $SESSION_ID started at $(date)" >> /tmp/agent-session.log

  session_end:
    - type: command
      timeout: 10
      command: |
        INPUT=$(cat)
        SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
        REASON=$(echo "$INPUT" | jq -r '.reason // "unknown"')
        echo "Session $SESSION_ID ended ($REASON) at $(date)" >> /tmp/agent-session.log

Response logging (stop hook)

hooks:
  stop:
    - type: command
      timeout: 10
      command: |
        INPUT=$(cat)
        SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
        RESPONSE_LENGTH=$(echo "$INPUT" | jq -r '.stop_response // ""' | wc -c | tr -d ' ')
        echo "[$(date)] Session $SESSION_ID - Response: $RESPONSE_LENGTH chars" >> /tmp/responses.log

Error notifications

hooks:
  notification:
    - type: command
      timeout: 10
      command: |
        INPUT=$(cat)
        LEVEL=$(echo "$INPUT" | jq -r '.notification_level // "unknown"')
        MESSAGE=$(echo "$INPUT" | jq -r '.notification_message // "no message"')
        echo "[$(date)] [$LEVEL] $MESSAGE" >> /tmp/agent-notifications.log
The notification hook fires when:
  • All models fail to respond
  • A degenerate tool call loop is detected
  • The max_iterations limit is reached

CLI flags

Add hooks from the command line without modifying the YAML file:
FlagDescription
--hook-pre-tool-useRun a command before every tool call
--hook-post-tool-useRun a command after every tool call
--hook-session-startRun a command when a session starts
--hook-session-endRun a command when a session ends
--hook-on-user-inputRun a command when waiting for input
All flags are repeatable.
# Add an audit hook without touching the YAML
docker agent run agent.yaml --hook-post-tool-use "./audit.sh"

# Combine multiple hooks
docker agent run agent.yaml \
  --hook-pre-tool-use "./validate.sh" \
  --hook-post-tool-use "./log.sh"

# Layer hooks onto a registry agent
docker agent run agentcatalog/coder \
  --hook-pre-tool-use "./audit.sh"
CLI hooks are appended to hooks defined in the YAML config — they don’t replace existing hooks. CLI pre/post-tool-use hooks match all tools (equivalent to matcher: "*").

Build docs developers (and LLMs) love