Hooks
Hooks let you run your own shell commands when something interesting happens inside AgentLoop — a task moves to review, an agent finishes work, the orchestrator starts up, and so on. They’re the primary extension point for wiring AgentLoop into the rest of your toolchain (Slack notifications, deploys, custom logging, external trackers) without writing a plugin.
Hooks are an alpha feature. The event list is stable, but new events may be added in upcoming releases.
Why use hooks
A hook is just a shell command that AgentLoop runs when an event fires. The command receives a JSON payload describing the event on stdin, so you can pipe it into jq, a script, or a small program of your choice.
Common reasons to add a hook:
- Post a Slack or Discord message when a task gets blocked
- Kick off a deploy when a task is marked done
- Mirror task activity into an external tracker
- Run a security scan whenever code lands in the review column
- Capture custom metrics about agent runtimes and outcomes
If you need richer behavior than a shell command can provide — for example, dynamically registering new agents — write a plugin instead.
Where hooks are configured
Hooks live under the hooks: section of your AgentLoop config file. Both user-level and project-level configs are supported, and definitions are additive: if you have hooks at both levels, all of them run.
| Location | Scope |
|---|---|
~/.config/agentloop/config.yaml | Hooks that should fire on every project on this machine |
./.agentloop/config.yaml | Hooks specific to one project (commit these so the team shares them) |
Project-level hooks run before user-level hooks for the same event.
Treat project-level hook scripts the same way you treat any executable in your repo. AgentLoop will run them on your machine when events fire — review hooks before pulling changes from collaborators.
Available events
| Event | Fires when |
|---|---|
task:created | A new task is added to the board |
task:updated | A task’s metadata changes |
task:statusChanged | A task moves between columns (todo, in-progress, review, done, blocked) |
task:assigned | A task is assigned to an agent |
task:completed | A task is marked done |
task:deleted | A task is removed |
agent:beforeInvoke | An agent is about to start work on a task |
agent:afterInvoke | An agent finishes a turn (success or failure) |
agent:error | An agent errors out |
orchestrator:started | The orchestrator (the loop that schedules agents) starts |
orchestrator:stopped | The orchestrator stops |
session:completed | An interactive chat session finishes |
Configuring a hook
A hook definition has two required fields — event and command — plus a handful of optional knobs for filtering, timeouts, and execution behavior.
# ./.agentloop/config.yaml
hooks:
settings:
enabled: true
default_timeout_ms: 30000
log_output: true
fail_silently: true
definitions:
- event: "task:statusChanged"
command: "./scripts/notify-slack.sh"
description: "Ping the team when something gets blocked"
matcher:
to_status: ["blocked"]Definition fields
| Field | Required | Description |
|---|---|---|
event | yes | One of the events listed above |
command | yes | Shell command to run (executed via sh -c) |
description | no | Human-readable label, shown in logs |
matcher | no | Filter that limits when this hook fires (see below) |
timeout_ms | no | Per-hook timeout. Falls back to settings.default_timeout_ms |
blocking | no | If true, AgentLoop waits for the script to finish. Defaults to fire-and-forget |
priority | no | Higher numbers run first when multiple hooks match the same event |
environment | no | Extra environment variables passed to the script |
Settings
| Setting | Default | Description |
|---|---|---|
enabled | true | Master kill switch for all hooks |
default_timeout_ms | 30000 | Default per-hook timeout |
log_output | true | Capture stdout/stderr from hooks in the AgentLoop logs |
fail_silently | true | If false, blocking hooks that exit non-zero surface the error to the caller |
Filtering with matcher
Most hooks only care about a slice of events. The matcher block narrows down when a hook fires — all conditions are combined with AND.
| Matcher field | Applies to | Description |
|---|---|---|
from_status | task:statusChanged | Only fire when moving from one of these columns |
to_status | task:statusChanged | Only fire when moving into one of these columns |
agent_type | task and agent events | Limit to specific agents (e.g. engineer, qa-tester) |
task_priority | task events | Limit to certain priorities (low, medium, high, critical) |
task_tags | task events | Fire when the task carries any of these tags |
success | agent:afterInvoke, agent:error | Filter by success/failure |
What a hook receives
When a hook fires, AgentLoop spawns a shell, sets a couple of environment variables, and writes a JSON payload to the script’s stdin.
Environment
| Variable | Description |
|---|---|
AGENTLOOP_HOOK_EVENT | The event name (e.g. task:statusChanged) |
AGENTLOOP_PROJECT_PATH | Absolute path of the project the event came from |
The script also runs with cwd set to the project root, so relative paths in your command (./scripts/foo.sh, ~/bin/foo.sh) resolve sensibly.
JSON payload on stdin
The exact shape varies by event, but every payload includes event, timestamp, timestamp_iso, and project_path. Task events include the full task object; status changes include previousStatus and newStatus; agent events include agentType, executionId, and friends.
A task:statusChanged payload, for example, looks roughly like:
{
"event": "task:statusChanged",
"timestamp": 1746320000000,
"timestamp_iso": "2026-05-04T00:00:00.000Z",
"project_path": "/Users/you/projects/my-app",
"task": {
"id": 42,
"title": "Implement authentication",
"status": "review"
},
"previousStatus": "in-progress",
"newStatus": "review"
}A simple script can pull fields out of the payload with jq:
#!/usr/bin/env bash
# scripts/notify-slack.sh
payload=$(cat)
title=$(echo "$payload" | jq -r '.task.title')
to=$(echo "$payload" | jq -r '.newStatus')
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
--data "{\"text\":\"Task moved to *$to*: $title\"}"Examples
Notify Slack when a task gets blocked
hooks:
definitions:
- event: "task:statusChanged"
command: "./scripts/notify-slack.sh"
description: "Slack alert on blocked tasks"
matcher:
to_status: ["blocked"]Trigger a deploy when work ships
hooks:
definitions:
- event: "task:statusChanged"
command: "./scripts/deploy.sh"
description: "Kick off staging deploy"
blocking: true
timeout_ms: 120000
matcher:
from_status: ["review"]
to_status: ["done"]
task_tags: ["deploy"]Audit log of every agent run
hooks:
definitions:
- event: "agent:afterInvoke"
command: "jq -c . >> ~/.agentloop-audit.jsonl"
description: "Append every agent run to a JSONL audit log"Limits and gotchas
- Hooks run with your full shell environment. They can do anything your user account can do. Treat them like any other script in your repo.
- Non-blocking by default. Hooks fire-and-forget unless you set
blocking: true. Use blocking only when downstream agents need to wait on the result. - Timeouts are enforced. Hooks that exceed
timeout_ms(default 30 seconds) are killed. - Output is captured for logging. Stdout and stderr are written to AgentLoop’s logs when
log_outputis on. Don’t echo secrets. - CI environment variables are stripped. AgentLoop removes
CI,CONTINUOUS_INTEGRATION,BUILD_NUMBER, andGITHUB_ACTIONSfrom the hook environment so tools don’t accidentally enter non-interactive CI mode. - Restart after editing. Hook configuration is loaded on startup. Restart the daemon (or quit and reopen the desktop app) after changing your config.