The worker daemon
The worker turns Kanon's UI into an execution surface: anyone on your team clicks Run with worker on a pending change, and a PR comes back — no one opens Claude Code, no one copies a command. The daemon runs on a machine you control, uses your Claude subscription, and your code never leaves that machine. Kanon receives one-line activity labels and the PR URL, nothing else.
How it works
Kanon UI: "Run with worker" on a change
→ task queued (visible immediately as a chip on the change card)
→ your daemon polls, claims the task
→ creates a git worktree on a server-derived branch
→ runs /kanon:work <change> --non-interactive headlessly
→ streams a live activity trace to the session view
→ opens a draft-or-ready PR via gh
→ reports the PR URL back to the change cardOne task runs at a time, each in its own worktree under .kanon/worktrees/,
so a running task never touches your checkout or another task's files.
Prerequisites
- The plugin installed and signed in (
/kanon:setup) in the repo the worker will serve — the daemon reads.kanon/config.jsonfor the repo slug and~/.kanon/credentials.jsonfor the token. claude— the daemon spawns headless Claude Code sessions; this machine's subscription pays for them.ghauthenticated (gh auth status) — the worker opens PRs with it.- A clean clone with an
originremote it can fetch from.
Start it
From a Claude Code session in the repo:
/kanon:workerOr directly from a shell (what the skill runs for you):
node <plugin-root>/mcp-server/dist/cli.js daemonThe daemon fails loudly at startup if claude, gh, or git aren't ready —
it will not claim a task it can't finish. Once polling, the Worker card on
/connect shows it online, and Run with worker buttons light up on the
Changes page within ~10 seconds.
Stop it with Ctrl-C. A task in flight fails honestly via lease expiry (~2 minutes) and can be re-queued — nothing silently re-runs.
The whole pipeline, from the UI
With a worker online, every stage of the loop runs from the browser:
| Stage | Where | What it queues |
|---|---|---|
| Discover | Review page → "Run discovery with worker" (or "Re-discover") | /kanon:discover --non-interactive, code-only — proposals land on the review page |
| Review | Review page | Always UI — approve, edit, reject; the human gate never moves |
| Scan | Review page → "Scan all with worker" (appears once review is complete) | /kanon:scan --non-interactive — every approved feature, with the freshness pre-skip |
| Work | Changes page → "Run with worker" | /kanon:work <change> --non-interactive — a review-ready PR |
Discover and scan tasks run in a detached worktree (they read code and push bundles; no branch, no PR) and every task type gets the same live session view. One discovery, one sweep, and one task per change can be in flight at a time — double-clicks land on the already-running session.
Queue work from the UI
On Changes (/knowledge-base/{repo-id}/changes), every open change shows
Run with worker while a worker is online. Clicking it:
- Queues a task and derives the branch name server-side (
fix/…,improve/…,feat/…— the same convention/kanon:workuses), so the card, the session view, and the PR all agree before work starts. - The change card shows a live chip (
worker: queued → running) linking to the session view — a step-by-step trace of what the worker is doing. - On success, the PR link lands on the change and in the session view. On failure, the session view shows what went wrong and the worker keeps the worktree for debugging.
Cancel from the session view: a queued task cancels instantly; a running one is asked to stop and acknowledges within a heartbeat (~2s).
Only one task per change can be in flight — double-clicks and racing teammates get the existing task, never a duplicate.
What the worker sends to Kanon (and what it never sends)
| Leaves your machine | Never leaves |
|---|---|
| One-line activity labels ("Read: src/cart.ts", "Bash: pnpm test") | File contents — tool inputs are whitelisted to paths/patterns only |
| The PR URL and number | Diffs, code, test output bodies |
| Task status + error summaries | Your Claude or GitHub credentials |
The task payload going the other way is structured too: the daemon only ever interpolates validated ids and enum flags into a command — free text typed into the Kanon UI cannot reach a shell on your machine.
Run it always-on
A worker on a laptop stops when the lid closes; queued tasks wait until it's back. For a team, run it on a box that stays up.
macOS (launchd) — ~/Library/LaunchAgents/com.kanon.worker.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.kanon.worker</string>
<key>WorkingDirectory</key><string>/path/to/your/repo</string>
<key>ProgramArguments</key><array>
<string>/usr/local/bin/node</string>
<string>/path/to/plugin/mcp-server/dist/cli.js</string>
<string>daemon</string>
</array>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/kanon-worker.log</string>
<key>StandardErrorPath</key><string>/tmp/kanon-worker.log</string>
</dict></plist>launchctl load ~/Library/LaunchAgents/com.kanon.worker.plistLinux (systemd) — /etc/systemd/system/kanon-worker.service:
[Unit]
Description=Kanon worker daemon
After=network-online.target
[Service]
User=ci
WorkingDirectory=/path/to/your/repo
ExecStart=/usr/bin/node /path/to/plugin/mcp-server/dist/cli.js daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsystemctl enable --now kanon-workerWhatever runs the service needs the same auth as an interactive run: a
signed-in ~/.kanon/credentials.json (or KANON_API_TOKEN), gh auth, and
Claude Code auth for that user.
Behavior details
- Timeout: a task is killed after 30 minutes (
KANON_TASK_TIMEOUT_MINUTESoverrides) and reported as failed. - Draft by default: in non-interactive mode the PR is a draft unless typecheck, lint, and tests all passed — a human promotes it.
- Nothing auto-merges. Ever.
- Identity: the daemon mints a stable worker id into
~/.kanon/worker.jsonon first run; revoking the API token in Settings → Tokens cuts it off immediately.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| "Run with worker" doesn't appear | No fresh heartbeat: the daemon isn't running, or its token maps to a different workspace. Check the Worker card on /connect. |
| Daemon exits immediately with 401/403 | Token revoked or workspace-scoped token missing — re-run /kanon:setup. The daemon deliberately refuses to retry auth failures. |
| Task stuck "queued" | Worker offline (lid closed?) — tasks wait; start the daemon and it claims within one poll (~5s). |
| Task failed with "worker lease expired" | The daemon died mid-task (crash, network, shutdown). The worktree is kept under .kanon/worktrees/<task-id> for inspection; re-queue when ready. |
| PR opened but the card shows no link | The transcript didn't include the URL and gh pr view failed in the worktree — check gh auth status on the worker box. |
worker id is registered to another workspace | ~/.kanon/worker.json was copied between machines signed into different workspaces — delete it; a fresh id is minted on next start. |