Chorus Plugin for Claude Code

July 30, 2026 · View on GitHub

Overview

The Chorus Plugin packages the Chorus Skill with Hooks for automatic session lifecycle management. Hooks guarantee execution at specific Claude Code lifecycle events, removing the dependency on Claude "remembering" to manage sessions.

Architecture: Hook + Skill Division

Hooks (automatic, no intelligence needed):
  SessionStart  → chorus checkin + session discovery (inject context)
  SubagentStart → create Chorus session + write session file (SYNC)
  TeammateIdle  → session heartbeat
  SubagentStop  → auto-checkout all tasks + close Chorus session
  TaskCompleted → checkout via metadata bridge

Skill + MCP (requires LLM judgment):
  Task claiming, checkin_task, status updates, report_work, proposals

Sub-agent Session Discovery (Plan A)

Sub-agents discover their Chorus session UUID without Team Lead intervention:

Team Lead spawns sub-agent "frontend-worker"

  ├─ [Hook: SubagentStart] fires SYNCHRONOUSLY before sub-agent runs
  │   ├─ Creates Chorus session via MCP
  │   ├─ Writes <sessionId>/sessions/frontend-worker.json (under ~/.chorus/plugin/<cwd-slug>/)
  │   └─ Output to Team Lead: "Session UUID: xxx"

  └─ Sub-agent starts
      ├─ [Hook: SessionStart] fires (if applicable)
      │   └─ Scans this session's sessions/ dir → outputs "Your session: xxx"

      └─ OR: Sub-agent reads <sessionId>/sessions/frontend-worker.json
          (instructed by skill docs or Team Lead prompt)

The sub-agent gets its session UUID via two redundant paths:

  1. SessionStart hook scans session files and outputs them as context
  2. File read — the sub-agent reads its session's sessions/<my-name>.json directly

Auto-cleanup on Sub-agent Exit (Plan D)

When a sub-agent exits, the SubagentStop hook automatically:

  1. Queries the Chorus session for active task checkins
  2. Checks out from every checked-in task
  3. Closes the Chorus session
  4. Removes the session file and state entries

This means sub-agents never leave behind dangling checkins or open sessions.

Installation

1. Configure Environment

Set the following environment variables (e.g., in .env or your shell profile):

export CHORUS_URL="https://chorus.example.com"   # or http://localhost:8637
export CHORUS_API_KEY="cho_your_api_key_here"

2. Install Skill + Plugin

See the install instructions in public/chorus-plugin/skills/chorus/SKILL.md for skill details. The skill is bundled with the plugin and delivered automatically with plugin updates.

For local development within this repo, the skill is already symlinked:

.claude/skills/chorus → ../../public/chorus-plugin/skills/chorus

3. Load the Plugin

For external users who downloaded via the install script:

CHORUS_URL=<url> CHORUS_API_KEY=cho_xxx claude --plugin-dir .chorus-plugin

For local development within this repo:

CHORUS_URL=http://localhost:8637 CHORUS_API_KEY=cho_xxx claude --plugin-dir public/chorus-plugin

4. MCP Server

The plugin includes a .mcp.json template that configures the Chorus MCP server. It uses $CHORUS_URL and $CHORUS_API_KEY from the environment.

File Layout

public/chorus-plugin/                # Plugin root
├── .claude-plugin/plugin.json       # incl. userConfig: enable{Proposal,Task,Code}Reviewer + max*ReviewRounds
├── hooks/hooks.json
├── agents/                          # Read-only reviewer sub-agents (VERDICT: PASS / PASS WITH NOTES / FAIL)
│   ├── proposal-reviewer.md         # after chorus_pm_submit_proposal → verdict on the proposal
│   ├── task-reviewer.md             # after chorus_submit_for_verify → verdict on the task
│   └── code-reviewer.md             # final ship gateway: Idea's aggregate change → verdict on the idea
├── bin/                             # Hook scripts
│   ├── chorus-api.sh               # Shared API + state + session file helpers
│   ├── on-session-start.sh         # SessionStart: checkin + session discovery
│   ├── on-subagent-start.sh        # SubagentStart: create session + write file (SYNC)
│   ├── on-subagent-stop.sh         # SubagentStop: checkout tasks + close + cleanup
│   ├── on-teammate-idle.sh         # TeammateIdle: heartbeat
│   ├── on-task-completed.sh        # TaskCompleted: checkout via metadata bridge
│   ├── on-post-submit-proposal.sh  # PostToolUse: remind to spawn proposal-reviewer
│   ├── on-post-submit-for-verify.sh # PostToolUse: remind to spawn task-reviewer
│   ├── on-post-verify-task.sh      # PostToolUse: archive (A) + code-review gateway (C) + completion report (B) reminders
│   └── tests/                       # Fixture-based hook tests (test-on-post-verify-task.sh)
├── skills/chorus/                   # Skill files
│   ├── SKILL.md
│   ├── package.json
│   └── references/                  # Role-specific workflow docs
└── .mcp.json

.claude/skills/
└── chorus → ../../public/chorus-plugin/skills/chorus  (symlink, local dev)

Runtime state (global, not per-project):
~/.chorus/plugin/<cwd-slug>/<sessionId>/   # <cwd-slug> = project dir with '/'→'-'
├── state.json                       # Hook state: agent→session mappings
├── sessions/                        # Per-agent session files (Plan A)
│   ├── frontend-worker.json
│   └── backend-worker.json
├── pending/                         # PreToolUse:Task → SubagentStart handoff
└── claimed/                         # claimed pending files, keyed by agent_id

Local hook state lives under the global ~/.chorus/plugin/ root (aligned with the daemon's ~/.chorus/ convention), partitioned by a readable project slug and the Claude Code session id — so there is no per-project .chorus/ folder to create or gitignore. Path resolution is centralized in bin/chorus-paths.sh. This holds only hook-to-hook scratchpad state (session mappings, cached owner/permissions, sub-agent handoff files) — never credentials, and never a project↔directory binding (project scoping is server-side via the X-Chorus-Project header / explicit projectUuid args).

Hooks

SessionStart

Trigger: Claude Code session starts or resumes.

Behavior:

  1. Checks if CHORUS_URL and CHORUS_API_KEY are set
  2. Calls chorus_checkin via MCP to verify connectivity and inject agent context
  3. Outputs hook status and usage hints
  4. If resuming with existing state, sends a heartbeat
  5. Session discovery (Plan A): Scans this session's sessions/ dir (under ~/.chorus/plugin/<cwd-slug>/<sessionId>/) for pre-created session files and outputs them. If the current agent is a sub-agent, it can identify its own session by name.

SubagentStart

Trigger: A sub-agent (teammate) is spawned via the Task tool.

SYNCHRONOUS — completes before the sub-agent starts executing.

Behavior:

  1. Reads agent_id, agent_name, and agent_type from the event
  2. Skips non-worker types (Explore, Plan, haiku, etc.)
  3. Creates a Chorus session via MCP (chorus_create_session)
  4. Stores mappings in state.json (by agent_id and agent_name)
  5. Writes session file to <sessionId>/sessions/<agent_name>.json (under ~/.chorus/plugin/<cwd-slug>/) with full session info
  6. Outputs session UUID and file path to Team Lead's context

SubagentStop

Trigger: A sub-agent exits.

Behavior:

  1. Reads agent_id and agent_name from the event
  2. Looks up the session UUID from state
  3. Auto-checkout (Plan D): Queries chorus_get_session for active task checkins, then calls chorus_session_checkout_task for each
  4. Closes the Chorus session via MCP
  5. Cleans up state entries and session file

TeammateIdle

Trigger: A teammate goes idle (between turns).

Behavior:

  1. Reads agent_id or teammate_name from the event
  2. Looks up the session UUID from state (by agent_id or name)
  3. Sends a heartbeat via chorus_session_heartbeat

This prevents Chorus sessions from being marked inactive during long-running agent team operations.

TaskCompleted

Trigger: A Claude Code task is marked completed.

Behavior:

  1. Reads task info from the event
  2. Searches for chorus:task:<uuid> pattern in the task description/subject
  3. If found, checks out the corresponding session from that Chorus task

State File

The plugin stores session mapping state in ~/.chorus/plugin/<cwd-slug>/<sessionId>/state.json (global, per Claude Code session — not per-project). <cwd-slug> is the project directory with non-alphanumeric characters replaced by - (mirroring Claude Code's own ~/.claude/projects/ encoding). The file is created automatically; being outside the repo, it never needs gitignoring.

Format:

{
  "main_session_uuid": "abc-123-...",
  "session_<agent_id>": "<chorus-session-uuid>",
  "session_<agent_name>": "<chorus-session-uuid>",
  "agent_for_session_<session-uuid>": "<agent-id>"
}

Session Files (Plan A)

Per-agent session files live in ~/.chorus/plugin/<cwd-slug>/<sessionId>/sessions/<agent_name>.json:

{
  "sessionUuid": "abc-123-def-456",
  "agentId": "agent_abc123",
  "agentName": "frontend-worker",
  "agentType": "general-purpose",
  "chorusUrl": "http://localhost:8637",
  "createdAt": "2026-02-16T01:00:00Z"
}

These files are:

  • Created by SubagentStart hook (synchronously, before sub-agent runs)
  • Read by SessionStart hook (output as context) and by sub-agents directly
  • Deleted by SubagentStop hook (cleanup on exit)

Metadata Bridge

To link a Claude Code task with a Chorus task, include the pattern chorus:task:<uuid> in the Claude Code task description. For example:

TaskCreate:
  subject: "Implement login API"
  description: "Build the /api/login endpoint. chorus:task:a1b2c3d4-e5f6-7890-abcd-ef1234567890"

When the Claude Code task is completed, the TaskCompleted hook will automatically check out from the linked Chorus task.

chorus-api.sh Commands

CommandDescription
checkinCheck connectivity with Chorus backend
mcp-tool <name> [args_json]Call any MCP tool via JSON-RPC
state-get <key>Read a value from state.json
state-set <key> <value>Write a value to state.json
state-delete <key>Delete a key from state.json
session-read <name>Read a session file for a named agent
session-listList all pre-created session files as JSON

What Hooks Automate vs. What LLM Still Does

OperationWhoHow
Create Chorus sessionHook (SubagentStart)Automatic, sync
Session heartbeatHook (TeammateIdle)Automatic, async
Close Chorus sessionHook (SubagentStop)Automatic, async
Checkout all tasks on exitHook (SubagentStop)Automatic, async
Checkout on CC task doneHook (TaskCompleted)Automatic, needs chorus:task:<uuid>
Discover session UUIDHook (SessionStart) + fileAutomatic via session files
Checkin to a taskLLMchorus_session_checkin_task
Move task statusLLMchorus_update_task
Report workLLMchorus_report_work (needs LLM to summarize)
Submit for verifyLLMchorus_submit_for_verify (needs judgment)
Claim/release tasksLLMchorus_claim_task / chorus_release_task

Prerequisites

  • curl — for REST API calls
  • jq — for JSON parsing (recommended; basic fallback available without it)
  • CHORUS_URL and CHORUS_API_KEY environment variables

Troubleshooting

Hook not firing

  1. Verify the plugin is loaded: claude --plugin-dir public/chorus-plugin (or .chorus-plugin for external installs)
  2. Check that hooks.json is valid JSON
  3. Ensure hook scripts are executable (chmod +x bin/*.sh)

"CHORUS_URL is not set"

Set the environment variables before starting Claude Code:

export CHORUS_URL="http://localhost:8637"
export CHORUS_API_KEY="cho_your_key"

Session not created for sub-agent

The hook skips read-only agent types (Explore, Plan, haiku). Only worker agents (general-purpose, Bash) get Chorus sessions.

Sub-agent can't find its session

  1. Check that ~/.chorus/plugin/<cwd-slug>/<sessionId>/sessions/<agent-name>.json exists
  2. Verify SubagentStart hook ran (check Team Lead's context for "Chorus session auto-created" message)
  3. Ensure the sub-agent name matches the filename

State file not updating

Check that $HOME is set and writable. The state file is at ~/.chorus/plugin/<cwd-slug>/<sessionId>/state.json (if $HOME is unset it falls back to /tmp; if the Claude Code session id is unavailable it uses a shared <cwd-slug>/no-session/ bucket).

Heartbeat failures

If heartbeats fail silently, check:

  1. Network connectivity to Chorus
  2. API key validity (keys expire if rotated)
  3. Session may have been manually closed