Hooks
Lifecycle hooks and event handling in swarm.
Overview
Hooks are shell commands that execute in response to lifecycle events during a swarm session. They can observe events (logging, notifications), modify behavior (blocking tool calls), or perform side effects (running scripts). Hooks are configured in ~/.swarm/settings.json under the hooks section.
How Hooks Work
- An event fires (e.g.,
PreToolUsebefore a tool executes) - Swarm finds matching hooks by checking each
MatcherGroupfor the event - Matching hooks receive a
HookContextJSON object on stdin - Sync hooks block until they return a
HookDecision; async hooks fire and forget - If any sync hook returns
DenyorBlock, the action is prevented
Step 1: Add Hooks to Configuration
Hooks are organized by event type in the hooks section:
{
"hooks": {
"pre_tool_use": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./scripts/validate-bash.sh",
"timeout": 10
}
]
}
],
"session_start": [
{
"hooks": [
{
"type": "command",
"command": "./scripts/on-session-start.sh",
"async": true
}
]
}
]
}
}
Event Types
Swarm supports 13 hook event types:
| Event Type | Config Key | When It Fires |
|---|---|---|
SessionStart | session_start | A new agent session begins |
PreToolUse | pre_tool_use | Before a tool executes |
PostToolUse | post_tool_use | After a tool executes successfully |
PostToolUseFailure | post_tool_use_failure | After a tool execution fails |
Stop | stop | Session stop is initiated |
SessionEnd | session_end | An agent session ends |
SubagentStart | subagent_start | A subagent is spawned |
SubagentStop | subagent_stop | A subagent stops |
TeammateIdle | teammate_idle | An agent has been idle past the nudge threshold |
TeammateIdleWarning | teammate_idle_warning | An agent has been idle past the warning threshold |
TaskCompleted | task_completed | An agent completes a task |
Notification | notification | A notification is generated |
StallDetected | stall_detected | Agent stall detected (no heartbeat past threshold) |
Step 2: Define Matcher Groups
Each event type has an array of MatcherGroup entries. A matcher group specifies:
matcher(optional) — A regex pattern matched against the tool name or event source. If omitted, matches all events of that type.hooks— An array of hook handlers to execute when matched.
{
"pre_tool_use": [
{
"matcher": "Bash",
"hooks": [ ... ]
},
{
"matcher": "Edit|Write",
"hooks": [ ... ]
},
{
"hooks": [ ... ]
}
]
}
The first group matches only Bash tool calls, the second matches Edit or Write, and the third matches all tool calls (no matcher = match all).
Step 3: Write Hook Handlers
Each hook handler is a command type with these fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | "command" | Yes | — | Handler type (only command is supported) |
command | String | Yes | — | Shell command to execute (run via sh -c) |
timeout | u64 | No | 30 | Timeout in seconds |
async | bool | No | false | If true, fire-and-forget (no decision collected) |
status_message | String | No | null | Display message while hook runs |
Example:
{
"type": "command",
"command": "./scripts/lint-check.sh",
"timeout": 15,
"status_message": "Running lint check..."
}
Step 4: Write Hook Scripts
Hook scripts receive a HookContext JSON object on stdin and optionally output a HookDecision JSON object on stdout.
HookContext (stdin)
The context object sent to every hook:
{
"session_id": "20250115-a3f2",
"cwd": "/home/user/my-project/.swarm/worktrees/backend",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
},
"tool_response": null
}
| Field | Type | Present | Description |
|---|---|---|---|
session_id | String | Always | Active session ID |
cwd | String | Always | Agent working directory |
hook_event_name | String | Always | Event name (e.g., "PreToolUse") |
tool_name | String | Tool events only | Name of the tool |
tool_input | JSON | Tool events only | Tool input payload |
tool_response | JSON | PostToolUse only | Tool response payload |
Fields that don't apply to the event type are omitted from the JSON (not set to null).
Specialized Event Inputs
Some events provide additional context beyond the standard HookContext:
TeammateIdleWarning:
{
"event": "TeammateIdleWarning",
"agent_name": "backend",
"session_id": "20250115-a3f2",
"idle_duration_secs": 300,
"nudge_count": 2,
"last_state": "Running"
}
StallDetected:
{
"event": "StallDetected",
"agent_name": "backend",
"session_id": "20250115-a3f2",
"stall_duration_secs": 900,
"last_heartbeat_event": "PostToolUse"
}
HookDecision (stdout)
Sync hooks can output a decision JSON to control the action:
{ "decision": "allow" }
{ "decision": "deny", "reason": "Command not allowed by policy" }
{ "decision": "block", "reason": "Security violation detected" }
| Decision | Effect |
|---|---|
allow | Permits the action to proceed |
deny | Prevents the action (with reason) |
block | Prevents the action (with reason) |
Both deny and block prevent the action. The reason field is optional and logged for diagnostics.
Exit Code Behavior
| Exit Code | Behavior |
|---|---|
0 | Parse stdout for decision JSON. No output = implicit allow |
2 | Blocking error — treated as deny with stderr as reason |
| Other | Non-blocking error — logged but action proceeds |
Sync vs Async Hooks
| Mode | Blocks Action | Returns Decision | Use Case |
|---|---|---|---|
Sync (async: false) | Yes | Yes | Validation, policy enforcement |
Async (async: true) | No | No | Logging, notifications, side effects |
Sync hooks within a single event run in parallel. If any sync hook returns deny or block, the action is prevented and all denial reasons are aggregated.
Async hooks are spawned as background tasks. They don't contribute decisions and their success or failure doesn't affect the action.
Example Scripts
Pre-Tool-Use Validator
Block dangerous bash commands:
#!/bin/bash
# scripts/validate-bash.sh
# Blocks rm -rf and other dangerous commands
CONTEXT=$(cat)
COMMAND=$(echo "$CONTEXT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s+/'; then
echo '{"decision": "deny", "reason": "Refusing to run rm -rf on root paths"}'
exit 0
fi
echo '{"decision": "allow"}'
Session Start Logger
Log session starts to a file:
#!/bin/bash
# scripts/on-session-start.sh
CONTEXT=$(cat)
SESSION_ID=$(echo "$CONTEXT" | jq -r '.session_id')
echo "[$(date)] Session started: $SESSION_ID" >> /tmp/swarm-sessions.log
Post-Tool-Use Notifier
Send a notification after tool failures:
#!/bin/bash
# scripts/notify-failure.sh
CONTEXT=$(cat)
TOOL=$(echo "$CONTEXT" | jq -r '.tool_name')
echo "[$(date)] Tool failed: $TOOL" >> /tmp/swarm-failures.log
Stall Detector
Auto-restart stalled agents:
#!/bin/bash
# scripts/handle-stall.sh
CONTEXT=$(cat)
AGENT=$(echo "$CONTEXT" | jq -r '.agent_name')
DURATION=$(echo "$CONTEXT" | jq -r '.stall_duration_secs')
echo "[$(date)] Agent $AGENT stalled for ${DURATION}s" >> /tmp/swarm-stalls.log
Complete Configuration Example
{
"hooks": {
"session_start": [
{
"hooks": [
{
"type": "command",
"command": "./scripts/on-session-start.sh",
"async": true
}
]
}
],
"pre_tool_use": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./scripts/validate-bash.sh",
"timeout": 10,
"status_message": "Validating command..."
}
]
}
],
"post_tool_use_failure": [
{
"hooks": [
{
"type": "command",
"command": "./scripts/notify-failure.sh",
"async": true
}
]
}
],
"stall_detected": [
{
"hooks": [
{
"type": "command",
"command": "./scripts/handle-stall.sh",
"timeout": 5
}
]
}
]
}
}
Troubleshooting
Hook not firing
- Verify the event type key in config matches the event (e.g.,
pre_tool_use, notpreToolUse) - Check the
matcherregex matches the tool name or event source - Ensure the script is executable (
chmod +x scripts/my-hook.sh)
Hook timing out
- The default timeout is 30 seconds. Increase it with the
timeoutfield. - On timeout, the child process is killed and the hook is treated as a non-blocking error.
Decision not being applied
- Async hooks cannot return decisions — set
async: falsefor policy enforcement - Ensure exit code is
0and stdout contains valid JSON - Exit code
2is a special blocking error; other non-zero codes are non-blocking
Multiple hooks conflicting
Sync hooks for the same event run in parallel. If any returns deny or block, the action is prevented. Reasons from all denying hooks are aggregated.
Related
- Agent Lifecycle — When lifecycle events fire
- Permissions — How hooks interact with the permission system
- Config Schema — Full
HooksConfigreference