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.

hook lifecycle
# 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

.kanban/hooks/
# 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

EventExecutableWhenPayload fields
task.createdtask-createdTask dispatchedtask_id, title, role, priority, project
task.claimedtask-claimedAgent claims a tasktask_id, agent, title, project, priority, role_boundary
task.progresstask-progressProgress loggedtask_id, agent, note, note_type
task.completedtask-completedTask finishedtask_id, agent, title
task.submitted_for_reviewtask-submitted-for-reviewSubmitted for reviewtask_id, agent, title
task.transferredtask-transferredClaim transferredtask_id, agent (to), from_agent, title
task.blockedtask-blockedTask blockedtask_id, agent, reason
review.approvedreview-approvedApprovedtask_id, reviewer, note
review.rejectedreview-rejectedRejectedtask_id, reviewer, reason
task.priority_updatedtask-priority-updatedBatch prioritytask_ids, priority
task.project_updatedtask-project-updatedBatch projecttask_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

.kanban/hooks/task-completed.d/slack
#!/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

.kanban/hooks/task-completed.d/metrics
#!/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