Skip to content

Hooks Guide

Hooks let you execute actions when session states change. Use them to get notifications, trigger CI jobs, log events, or integrate with other systems.

Hook Types

Webhook

HTTP POST to a URL:

hooks:
  - name: my-webhook
    type: webhook
    trigger: state_change
    url: "https://your-server.com/webhook"

Shell

Execute a shell command:

hooks:
  - name: my-command
    type: shell
    trigger: state_change
    command: 'echo "$SESSION_NAME is $STATE"'

Triggers

Hooks fire based on triggers:

Trigger Fires When
state_change State or detail changes
program_change Detected program changes
any_change Any change (state or program)

State Change Examples

# When a session goes idle
- name: idle-notify
  trigger: state_change
  to_states: [Idle]
  command: 'notify-send "agentwatch" "$SESSION_NAME finished"'

# When a session hits an error
- name: error-alert
  trigger: state_change
  to_states: [Error]
  url: "https://alerts.example.com/webhook"

Program Change Examples

# When Claude Code starts (from shell)
- name: claude-started
  trigger: program_change
  from_programs: ["bash", "zsh"]
  to_programs: ["Claude Code"]
  command: 'echo "Claude Code started in $SESSION_NAME"'

# When any AI tool exits
- name: ai-exited
  trigger: program_change
  from_programs: ["Claude Code", "Codex"]
  to_programs: ["bash", "zsh"]
  command: 'echo "AI tool exited in $SESSION_NAME"'

Filters

Narrow which events trigger your hooks:

Filter Description
programs Only fire for these programs
states Only fire when in these states
from_states Only fire when transitioning FROM these states
to_states Only fire when transitioning TO these states
from_programs Only fire when changing FROM these programs
to_programs Only fire when changing TO these programs

Filter Examples

# Only Claude Code errors
- name: claude-errors
  trigger: state_change
  programs: ["Claude Code"]
  to_states: [Error]

# Only when becoming blocked from working
- name: blocked-from-working
  trigger: state_change
  from_states: [Working]
  to_states: [Blocked]

# Any state change for Claude Code or Codex
- name: ai-any-change
  trigger: state_change
  programs: ["Claude Code", "Codex"]

Include Formats

Control what data is sent with hooks:

Format Description
status Lightweight: program, state, detail only (default)
json Full JSON output from detection
text Plain text terminal capture
png PNG screenshot

Webhook Payloads

For webhooks, the format determines the POST body:

{
  "session_name": "coding",
  "program": "Claude Code",
  "state": "Error",
  "detail": "Command failed",
  "previous_state": "Working",
  "timestamp": "2026-02-19T10:30:00-05:00"
}
{
  "session_name": "coding",
  "program": {
    "name": "Claude Code",
    "version": "1.0.30",
    "confidence": 0.95
  },
  "state": {
    "state": "Error",
    "detail": "Command failed",
    "confidence": 0.90
  },
  "terminal": {
    "content": "...",
    "rows": 24,
    "cols": 80
  }
}
{
  "session_name": "coding",
  "program": "Claude Code",
  "state": "Error",
  "screenshot": "data:image/png;base64,..."
}

Shell Hook Files

For shell hooks, text and png create temporary files:

- name: capture-errors
  type: shell
  trigger: state_change
  to_states: [Error]
  include: text
  command: |
    # $CAPTURE_FILE contains the terminal content
    cp "$CAPTURE_FILE" "/var/log/agentwatch/$SESSION_NAME-error.txt"

Environment Variables

Shell hooks receive these environment variables:

Variable Description
SESSION_NAME tmux session name
SESSION_ID tmux session ID
PROGRAM Current program name
PROGRAM_VERSION Program version (if detected)
STATE Current state
DETAIL State detail
PREVIOUS_PROGRAM Previous program
PREVIOUS_STATE Previous state
PREVIOUS_DETAIL Previous detail
TIMESTAMP ISO timestamp
DURATION_SECONDS Time in previous state
CAPTURE_FILE Path to capture file (if include: text/png)
AGENTWATCH_TRIGGER Trigger type

Using Variables

- name: log-transitions
  type: shell
  trigger: state_change
  command: |
    echo "[$(date)] $SESSION_NAME: $PREVIOUS_STATE -> $STATE ($DETAIL)" \
      >> ~/agentwatch-transitions.log

Webhook Options

Additional options for webhooks:

- name: reliable-webhook
  type: webhook
  trigger: state_change
  url: "https://api.example.com/webhook"
  timeout_seconds: 10        # Request timeout (default: 10)
  retry_count: 3             # Retries on failure (default: 0)
  retry_delay_seconds: 1.0   # Delay between retries
  include: json

Multiple Hooks

You can define multiple hooks that fire on the same events:

hooks:
  # Log everything locally
  - name: local-log
    type: shell
    trigger: any_change
    command: 'echo "$TIMESTAMP $SESSION_NAME $PROGRAM $STATE" >> ~/agentwatch.log'

  # Webhook for errors only
  - name: error-webhook
    type: webhook
    trigger: state_change
    to_states: [Error]
    url: "https://api.example.com/errors"

  # Desktop notification for blocked
  - name: blocked-notify
    type: shell
    trigger: state_change
    to_states: [Blocked]
    command: 'notify-send "agentwatch" "$SESSION_NAME needs input"'

Use Case Examples

Desktop Notifications

# macOS
- name: macos-notify
  type: shell
  trigger: state_change
  to_states: [Idle, Blocked, Error]
  command: |
    osascript -e 'display notification "$SESSION_NAME: $STATE" with title "agentwatch"'

# Linux (libnotify)
- name: linux-notify
  type: shell
  trigger: state_change
  to_states: [Idle, Blocked, Error]
  command: 'notify-send "agentwatch" "$SESSION_NAME: $STATE ($DETAIL)"'

Slack/Discord Integration

- name: slack-notify
  type: webhook
  trigger: state_change
  to_states: [Blocked, Error]
  url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
  include: status

CI/CD Trigger

- name: trigger-tests
  type: webhook
  trigger: state_change
  programs: ["Claude Code"]
  to_states: [Idle]
  url: "https://ci.example.com/api/trigger"
  include: json

Error Screenshot Archive

- name: archive-errors
  type: shell
  trigger: state_change
  to_states: [Error]
  include: png
  command: |
    timestamp=$(date +%Y%m%d_%H%M%S)
    cp "$CAPTURE_FILE" "/var/log/agentwatch/errors/${SESSION_NAME}_${timestamp}.png"

Sound Alert

- name: blocked-sound
  type: shell
  trigger: state_change
  to_states: [Blocked]
  command: 'afplay /System/Library/Sounds/Glass.aiff'

Debugging Hooks

Dry Run Mode

Test hooks without executing them:

service:
  debug:
    enabled: true
    dry_run_hooks: true   # Log hooks but don't execute
    log_hooks: true       # Log hook execution details

View Hook Logs

# Check daemon logs for hook activity
grep "hook" ~/.cache/agentwatch/daemon.log

# Or use the logs command
agentwatch logs SESSION | grep hook

Test a Webhook

# Manually trigger a webhook to test your endpoint
curl -X POST https://your-server.com/webhook \
  -H "Content-Type: application/json" \
  -d '{"session_name": "test", "state": "Error", "detail": "Test error"}'

Hook Execution

Order

Hooks execute in the order defined in the config file.

Concurrency

Hooks run concurrently. A slow hook won't block others.

Failures

Failed hooks are logged but don't affect other hooks or the daemon.

# Check for hook failures
grep -i "hook.*fail\|hook.*error" ~/.cache/agentwatch/daemon.log

Timeouts

  • Webhooks timeout after timeout_seconds (default: 10)
  • Shell commands timeout after 30 seconds

Configuration Reference

Full hook schema:

hooks:
  - name: string              # Required: unique identifier
    type: webhook | shell     # Required: hook type
    trigger: state_change | program_change | any_change  # Required

    # Filters (all optional)
    programs: [string]        # Current program must match
    states: [string]          # Current state must match
    from_states: [string]     # Previous state must match
    to_states: [string]       # Current state must match
    from_programs: [string]   # Previous program must match
    to_programs: [string]     # Current program must match

    # Webhook options
    url: string               # Required for webhook
    timeout_seconds: number   # Default: 10
    retry_count: number       # Default: 0
    retry_delay_seconds: number  # Default: 1.0

    # Shell options
    command: string           # Required for shell

    # Both types
    include: status | json | text | png  # Default: status