Hooks
React to every state transition. Drop executable scripts in .kanban/hooks/ and extend the workflow without touching the binary.
How it works
Each event type maps to an executable file named after the event (dots replaced by dashes). When the event fires, the hook receives JSON on stdin with a 30-second timeout. If the hook exits non-zero, the error is logged to stderr but the kanban operation continues. Missing hooks are silently ignored.
# 1. An agent completes a task
$ kanban task complete TASK-1 --agent alice
# 2. Kanban fires task-completed hook with stdin:
{
"event": "task.completed",
"payload": {
"task_id": "TASK-1",
"agent": "alice",
"title": "Set up auth middleware",
"project": "default"
}
}
# 3. Hook runs (30s timeout). Non-zero exits logged, not fatal.
# 4. Missing hooks = silent skip. No error. Hook placement
# Single hook per event:
.kanban/hooks/
├── task-created
├── task-claimed
├── task-progress
├── task-completed
├── task-submitted-for-review
├── task-transferred
├── task-blocked
├── review-approved
├── review-rejected
├── task-priority-updated
└── task-project-updated
# Multiple hooks per event (concurrent execution):
.kanban/hooks/
├── task-completed
└── task-completed.d/
├── slack ← notifies Slack channel
├── metrics ← records metrics
└── dashboard ← updates live dashboard Chaining with .d/ directories
The single file runs synchronously before the operation returns. The .d/ hooks run concurrently in goroutines, so a slow Slack notifier won't delay the caller. Entries run in lexicographic order within each .d/ directory; each has its own 30-second timeout independent of the others.
Event reference
| Event | Executable | When | Payload fields |
|---|---|---|---|
task.created | task-created | Task dispatched | task_id, title, role, priority, project |
task.claimed | task-claimed | Agent claims a task | task_id, agent, title, project, priority, role_boundary |
task.progress | task-progress | Progress logged | task_id, agent, note, note_type |
task.completed | task-completed | Task finished | task_id, agent, title |
task.submitted_for_review | task-submitted-for-review | Submitted for review | task_id, agent, title |
task.transferred | task-transferred | Claim transferred | task_id, agent (to), from_agent, title |
task.blocked | task-blocked | Task blocked | task_id, agent, reason |
review.approved | review-approved | Approved | task_id, reviewer, note |
review.rejected | review-rejected | Rejected | task_id, reviewer, reason |
task.priority_updated | task-priority-updated | Batch priority | task_ids, priority |
task.project_updated | task-project-updated | Batch project | task_ids, project |
Writing hooks
Hooks must be executable (chmod +x). Any language works — bash, Python, Go, Node.js — as long as it reads stdin and completes within 30 seconds.
Example: Slack notification
#!/usr/bin/env bash
# Notify Slack when a task is completed
set -euo pipefail
read -r payload
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$(echo "$payload" | jq -c '{"text": "✅ *\(.payload.title)* completed by \(.payload.agent)"}')" Example: Metrics counter
#!/usr/bin/env bash
set -euo pipefail
read -r payload
task_id=$(echo "$payload" | jq -r '.payload.task_id')
agent=$(echo "$payload" | jq -r '.payload.agent')
# Append to local metrics log (rotated externally)
echo "$(date -Iseconds) completed $task_id by $agent" >> .kanban/metrics.log Hook contract
- Timeout: 30 seconds per hook. Hooks in
.d/directories each get their own 30s timeout (concurrent, not cumulative). - Errors: Non-zero exit is logged to stderr but does not fail the operation. The kanban command returns success regardless of hook failures.
- Missing: If the hook file or directory doesn't exist, it's silently skipped. No error, no warning.
- Permissions: Files must be executable (
chmod +x). Non-executable files in.d/are skipped.