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:
- SessionStart hook scans session files and outputs them as context
- File read — the sub-agent reads its session's
sessions/<my-name>.jsondirectly
Auto-cleanup on Sub-agent Exit (Plan D)
When a sub-agent exits, the SubagentStop hook automatically:
- Queries the Chorus session for active task checkins
- Checks out from every checked-in task
- Closes the Chorus session
- 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:
- Checks if
CHORUS_URLandCHORUS_API_KEYare set - Calls
chorus_checkinvia MCP to verify connectivity and inject agent context - Outputs hook status and usage hints
- If resuming with existing state, sends a heartbeat
- 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:
- Reads
agent_id,agent_name, andagent_typefrom the event - Skips non-worker types (Explore, Plan, haiku, etc.)
- Creates a Chorus session via MCP (
chorus_create_session) - Stores mappings in
state.json(by agent_id and agent_name) - Writes session file to
<sessionId>/sessions/<agent_name>.json(under~/.chorus/plugin/<cwd-slug>/) with full session info - Outputs session UUID and file path to Team Lead's context
SubagentStop
Trigger: A sub-agent exits.
Behavior:
- Reads
agent_idandagent_namefrom the event - Looks up the session UUID from state
- Auto-checkout (Plan D): Queries
chorus_get_sessionfor active task checkins, then callschorus_session_checkout_taskfor each - Closes the Chorus session via MCP
- Cleans up state entries and session file
TeammateIdle
Trigger: A teammate goes idle (between turns).
Behavior:
- Reads
agent_idorteammate_namefrom the event - Looks up the session UUID from state (by agent_id or name)
- 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:
- Reads task info from the event
- Searches for
chorus:task:<uuid>pattern in the task description/subject - 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
| Command | Description |
|---|---|
checkin | Check 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-list | List all pre-created session files as JSON |
What Hooks Automate vs. What LLM Still Does
| Operation | Who | How |
|---|---|---|
| Create Chorus session | Hook (SubagentStart) | Automatic, sync |
| Session heartbeat | Hook (TeammateIdle) | Automatic, async |
| Close Chorus session | Hook (SubagentStop) | Automatic, async |
| Checkout all tasks on exit | Hook (SubagentStop) | Automatic, async |
| Checkout on CC task done | Hook (TaskCompleted) | Automatic, needs chorus:task:<uuid> |
| Discover session UUID | Hook (SessionStart) + file | Automatic via session files |
| Checkin to a task | LLM | chorus_session_checkin_task |
| Move task status | LLM | chorus_update_task |
| Report work | LLM | chorus_report_work (needs LLM to summarize) |
| Submit for verify | LLM | chorus_submit_for_verify (needs judgment) |
| Claim/release tasks | LLM | chorus_claim_task / chorus_release_task |
Prerequisites
curl— for REST API callsjq— for JSON parsing (recommended; basic fallback available without it)CHORUS_URLandCHORUS_API_KEYenvironment variables
Troubleshooting
Hook not firing
- Verify the plugin is loaded:
claude --plugin-dir public/chorus-plugin(or.chorus-pluginfor external installs) - Check that
hooks.jsonis valid JSON - 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
- Check that
~/.chorus/plugin/<cwd-slug>/<sessionId>/sessions/<agent-name>.jsonexists - Verify SubagentStart hook ran (check Team Lead's context for "Chorus session auto-created" message)
- 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:
- Network connectivity to Chorus
- API key validity (keys expire if rotated)
- Session may have been manually closed