Choragos - Running a Team of Coding Agents Without Losing Control
// Choragos runs a team of AI coding agents with a real division of labour: an orchestrator that plans and delegates, workers with their own context, model and credentials, and a delegate/work-done protocol carrying work between them. A walkthrough of v0.11.2 with config recipes for real teams.
You have three Claude Code windows open. One is writing the feature, one is reviewing what the first one wrote, and one is supposed to run the release checks when the other two finish. You are the message bus. You copy a diff summary from window one into window two, you paste the review findings back, and every few minutes you tab through all three to see which one is sitting at a prompt waiting for an answer you did not know it needed.
That works for about twenty minutes. Then the reviewer finishes while you are reading the coder’s output, its findings scroll past, and the coder starts a second refactor on top of code the reviewer just flagged. Nobody is wrong. There is simply no protocol.
What is missing is not more parallelism. It is a division of labour and a contract. One agent should hold the plan and hand out work. The others should each get one task, in their own context, with their own model and their own credentials, and report back in a form the planner can act on. Running three agents side by side gives you none of that: it gives you three transcripts and a human in the middle turning one into another.
So the question is not how to drive three windows at once. It is what the team looks like, and what carries work between the seats.
This post walks through Choragos and its v0.11.2 release: the team model, the delegate/work-done protocol that moves work between roles, the config recipes I actually use, and where it falls short.
Who Should Read This?#
This post is for:
- Engineers running more than one coding agent at a time who are tired of being the copy-paste layer between them
- SREs and platform engineers who want agent work to be observable: what ran, how long it took, what it cost, and what can be rolled back
- Teams in regulated environments who need every agent prompt to pass through a gateway they control before it reaches a model provider
- Anyone who has tried to script agent coordination themselves and wants to see what the contract between roles should look like
What is Choragos?#
Choragos is a single Go binary that runs a team of AI coding agents. You define the team in a config file, one seat per role: an orchestrator that plans and delegates and never implements, and workers that each get a brief, a model, and a scope. Work moves between them over a protocol the agents themselves invoke, and the deck tracks every task on a board.
The name is the Greek χορηγός (choragos), the citizen who led and financed the chorus in classical Athens. Here it leads a chorus of agents.
Four things it gives you that three parallel windows do not:
- A division of labour you declare. The orchestrator holds the plan. Each worker is a separate process with its own context, its own model, and its own environment, so a reviewer is genuinely a second opinion rather than the same model grading its own homework.
- A contract between the seats.
choragos delegate --to coder --task "..."andchoragos work-doneare CLI commands the agents call. Every delegation gets a task ID and lands on a board, so an unfinished task is visible instead of silently dropped. - Control at the two moments that matter.
approve = truegates a delegation before the work happens, and every delegation snapshots the workspace first, sochoragos rollback T3undoes exactly that task once you find out it went wrong. - Optionally, a compliance gateway in the data path. With Sphragis enabled, every agent’s LLM traffic is routed through a local gateway that redacts PII and writes a tamper-evident audit log, and delegation is refused while that gateway is down.
The gateway is optional. Choragos runs standalone and every feature degrades cleanly without it.
Why Not Subagents, or a Script Around tmux?#
| Aspect | Script it yourself | In-agent subagents | Choragos |
|---|---|---|---|
| Who plans the work | You | The parent agent | An orchestrator seat you brief |
| Work handoff | You write the protocol | Vendor’s own mechanism | delegate / work-done over a socket |
| Per-worker context | Shared or manual | Inherited from the parent | Separate process, separate context |
| Multiple vendors | Yes, if you script each one | No, single vendor | Yes, any binary on PATH |
| Per-role isolation | Whatever the shell exports | Parent’s environment | env_allow / env_deny per role |
| Human gate on a task | None | Vendor-dependent | approve = true per role |
| Undo a bad task | git stash and hope | None | choragos rollback T3 |
| Cost visibility | None | Per-session, vendor UI | Per-role, live, from the gateway |
| Compliance path | None | Vendor’s endpoint | Local gateway, fail-closed |
Bottom line: subagents are the right answer when one vendor’s agent can do the whole job in one context. Choragos is the right answer when the job wants different seats with different models, different privileges, and a record of who did what.
What Choragos is NOT#
- Not an agent. It does not talk to any model itself. It runs the CLIs you already have.
- Not a terminal multiplexer. It will not replace tmux for your shells. It manages agent panes and nothing else.
- Not a CI system. Work is delegated live in a session you can attach to, not queued in a pipeline.
- Not a vendor SDK wrapper. There are no provider hooks. Token telemetry comes from what the gateway observes on the wire.
- Not a sandbox.
env_denykeeps credentials out of a role’s environment. It does not stop an agent from reading files or shelling out.
Core Concepts#
The team model#
A role is a seat, and the config file is the org chart. Each seat declares the binary to run, the model to run it with, a prompt_template that becomes its standing brief, and the scope of what it is allowed to see.
Exactly one seat sets start = true. That is the orchestrator: it receives the delegation protocol in its boot brief and gets the work-done reports back. Everyone else is a worker that waits for a task.
The reason this beats one agent with a long prompt is context, not parallelism. Every worker is a separate process with a separate context window, so the reviewer never sees the coder’s exploration, the auditor never sees the reviewer’s draft, and none of them inherit the orchestrator’s plan. Splitting correctness from security across two seats gets noticeably better findings than asking one agent to do both, because neither review has to compete for attention inside a single context.
The teams doc has the numbers behind the other half of that argument: a Claude Code system prompt is around 44k tokens, your personal MCP servers add roughly 25k, a global CLAUDE.md another 10k, while the Choragos boot brief costs about 1.5k. Dropping MCP servers from workers with --strict-mcp-config takes a measured case from about 42% context consumed at boot down to about 30%. Workers do not need your Jira MCP. The orchestrator might.
Separate processes also mean separate privileges, which is the part that has no equivalent inside a single agent. env_allow switches a seat from inherit-everything to an allowlist; env_deny strips specific variables and always wins over an allowance. A reviewer that only reads code has no business holding AWS_*, and here it simply does not.
The delegate/work-done protocol#
The orchestrator agent does not have a special channel. It gets told, in its boot brief, that it can run shell commands, and those commands are choragos delegate and choragos work-done. Both connect to a Unix socket at $CHORAGOS_SOCK, which the deck injects into every role’s environment.
# what the orchestrator agent runs
choragos delegate --to coder --task "Add retry with jitter to the S3 client" \
--brief "See internal/store/s3.go. Existing backoff is linear."
# what the worker runs when it is finished
choragos work-done --id T3 --done \
--task "Added exponential backoff with jitter" \
--report .choragos/reports/T3.md
The socket is per-directory: <runtime dir>/choragos-<uid>/<8 hex of sha256(cwd)>.sock, mode 0600, with a five second deadline on the whole exchange. The directory hash is truncated hard because Unix socket paths cap near 104 bytes on macOS.
The wire contract is small enough to implement from any language:
{
"cmd": "delegate",
"to": ["coder"],
"task": "Add retry with jitter to the S3 client",
"brief": "See internal/store/s3.go. Existing backoff is linear."
}
The ack is {"status":"ok"}, and the docs are explicit that it means delivered to the deck, not accepted by the agent. That distinction matters when you are integrating: nothing about the socket exchange tells you the model did the work.
When a delegation lands, the deck assigns it a task id (T1, T2, …), checkpoints the workspace, writes the task file, and injects the pointer line into the target pane. When work-done comes back, the summary is injected into the orchestrator’s pane as A worker reports: ... and the board entry is resolved.
Approval gates and checkpoints#
Two different safety nets, at two different moments.
A gate fires before the work: set approve = true on a role and every delegation to it pauses in the deck until you press y or n. You can page the brief in-app with v, or open it in $EDITOR with e. A rejection is reported back to the orchestrator, which is the part that makes it useful, since the orchestrator can re-plan instead of hanging.
A checkpoint fires after you find out it went wrong. Every delegation snapshots tracked and untracked files (respecting .gitignore) as a parentless commit under refs/choragos/checkpoints/<epoch>-<task-id>. Your index, HEAD, branches, and stash are never touched, and no porcelain command will ever show these commits.
choragos checkpoints # task id, age, target role
choragos rollback T3 # restore files to the state before T3 ran
choragos checkpoints prune # apply the retention policy (default: keep 20)
rollback restores files and deletes ones the task created. It never moves HEAD or rewrites history, and it checkpoints the current state first, so choragos rollback pre-rollback-T3 undoes the undo.
Why this matters: the failure mode with parallel agents is not one dramatic mistake, it is a worker quietly rewriting a file another worker was mid-way through. Being able to name the task and restore exactly its blast radius is worth more than a
git stashyou have to reconstruct from memory.
Owned PTY panes, and why handoff does not race#
Everything above is a coordination model, and a coordination model is only as good as its delivery. This is the unglamorous layer that makes the rest reliable.
The obvious way to build this is one agent per tmux pane, driven with send-keys, polling the pane text to work out when an agent is ready for the next instruction. I built that first. It works right up until an agent’s TUI redraws its status line at the wrong moment, send-keys fires into a pane that has not finished booting, and half the instruction ends up as literal text in a prompt that never submits. You are polling a screen that lies to you.
Choragos instead spawns each agent with pty.StartWithSize and runs the output through a hinshun/vt10x emulator, so it holds the actual screen buffer rather than a scrape of it. That is how it knows an agent is waiting for input, working, or idle, and it is why boot injection is verified rather than fired blind: wait one second after start, wait for 1200ms of quiet output, inject the role brief, then check the screen to confirm the text landed. If it did not, retry after three seconds, up to two tries.
One detail in there is worth calling out, because it took a while to find:
// injectEnterDelay separates the typed text from its submit;
// Claude's TUI treats text+Enter in one write as a paste.
const injectEnterDelay = 200 * time.Millisecond
Write the text and the carriage return in a single PTY write and Claude Code treats it as a paste, which lands in the input box without submitting. Splitting the write by 200ms makes it typing. This is the class of bug that makes multiplexer scripting flaky, and you cannot fix it from outside the terminal.
The same constraint is why task content travels as a file rather than as injected text: multi-line pastes get dropped. A delegation writes .choragos/worker-task-<role>.md and injects a single line telling the worker to read it.
Architecture#
flowchart TB
User([You]) -->|attach / keys| Deck
subgraph Deck["Choragos Deck"]
TUI[Tiling WM + task board]
Core[Session core: roster, PTYs, gates]
Sock[(Unix socket 0600)]
end
subgraph Panes["Owned PTY panes"]
Orch[orchestrator]
Coder[coder]
Rev[reviewer]
Aud[auditor]
end
Core -->|PTY write| Orch
Core -->|PTY write| Coder
Core -->|PTY write| Rev
Core -->|PTY write| Aud
Orch -->|choragos delegate| Sock
Coder -->|choragos work-done| Sock
Rev -->|choragos work-done| Sock
Sock --> Core
Core -->|snapshot| Git[(refs/choragos/checkpoints)]
Orch -.->|ANTHROPIC_BASE_URL| GW
Coder -.->|/agent/coder| GW
Rev -.->|/agent/reviewer| GW
GW[Sphragis gateway] -.->|redacted| API[Model provider]
GW -.->|/metrics| Core
The session core is deliberately UI-free. internal/deck/session.go holds the roster, PTYs, tasks, gates, IPC and supervision with no bubbletea or lipgloss imports, and talks to the UI through a notify channel. That separation is what made headless mode and detach/attach possible without a rewrite.
Configuration Recipes#
Choragos reads .choragos.toml from the working directory. Without one it runs a built-in five-role team. Unknown keys become startup warnings rather than silent no-ops, which I appreciate more every time I typo a key.
Generate one with choragos init, or pick a template:
choragos init # commented starter
choragos init --template review # read-only review crew
choragos init --template claude-team # all-Claude
choragos init --template mixed-team # Claude + a Gemini CLI
choragos init --auto # detect go.mod/package.json/Cargo.toml/pyproject.toml
choragos doctor # check the setup before you start
Below are the configs I actually run, one per use case.
Recipe 1: the default trio#
Plan, implement, review. This is what choragos init writes, and it is the right starting point for almost everyone.
[[roles]]
name = "orchestrator"
command = "claude"
model = "opus"
start = true
prompt_template = "You coordinate the team. Plan and delegate only; never implement, review, or audit yourself."
[[roles]]
name = "coder"
command = "claude"
model = "opus"
# workers skip your personal MCP servers (context hygiene); delete to inherit them
args = ["--strict-mcp-config"]
prompt_template = "Implement the requested change. Run the project's tests before reporting done."
[[roles]]
name = "reviewer"
command = "claude"
model = "sonnet"
args = ["--strict-mcp-config"]
prompt_template = "Review the change for correctness and edge cases. Report findings only; do not modify code."
# the reviewer never needs your cloud credentials
env_deny = ["AWS_*", "GCP_*", "GITHUB_TOKEN"]
Exactly one role sets start = true. That role is the orchestrator: it receives the delegation protocol in its boot brief and gets the work-done reports.
The --strict-mcp-config on workers is the context-hygiene lever from the team model section: workers boot without your personal MCP servers, so more of their window is left for the task you delegated. Delete it if a worker genuinely needs one of them.
Result: three panes, one planner, a coder with your credentials, a reviewer without them.
Recipe 2: read-only review crew#
No role in this team is allowed to write code. I run this against a branch before a PR goes out, and it is the config I would hand to someone nervous about agents touching their repo.
[[roles]]
name = "orchestrator"
command = "claude"
model = "opus"
start = true
prompt_template = "You coordinate a read-only review. Split the work into correctness and security aspects, delegate, then merge the findings into one report. Never modify code."
[[roles]]
name = "reviewer"
command = "claude"
model = "opus"
args = ["--strict-mcp-config"]
prompt_template = "Review the change for correctness, edge cases, and maintainability. Report findings only; do not modify code."
[[roles]]
name = "auditor"
command = "claude"
model = "sonnet"
args = ["--strict-mcp-config"]
prompt_template = "Audit the change for security issues: injection, secret handling, unsafe defaults, dependency risk. Report findings only; do not modify code."
env_deny = ["AWS_*", "GCP_*", "GITHUB_TOKEN"]
[checkpoints]
enabled = false # nothing writes; skip the snapshots
Result: two independent reviews with separate contexts, merged by the orchestrator into one report. Splitting correctness from security across two seats gets noticeably better findings than asking one agent to do both, because neither review has to compete for attention inside a single context.
Recipe 3: mixed vendors, least privilege#
The role system does not care which vendor you use. command is any binary on PATH, and model is appended as --model <value>, a convention that happens to fit most agent CLIs. Here Claude implements and a Gemini CLI reviews, so the reviewer is genuinely a second opinion rather than the same model grading its own homework.
[[roles]]
name = "orchestrator"
command = "claude"
model = "opus"
start = true
prompt_template = "You coordinate the team. Plan and delegate only; never implement, review, or audit yourself."
[[roles]]
name = "coder"
command = "claude"
model = "opus"
args = ["--strict-mcp-config"]
prompt_template = "Implement the requested change. Run the project's tests before reporting done."
env_allow = ["ANTHROPIC_API_KEY"]
[[roles]]
name = "reviewer"
command = "agy"
model = "Gemini 3.1 Pro (High)"
prompt_template = "Review the change for correctness and edge cases. Report findings only; do not modify code."
env_allow = ["GEMINI_API_KEY", "GOOGLE_*"]
# teach the status heuristics this TUI's prompts if they differ from Claude's
input_prompts = ["continue? <enter>"]
chrome_markers = ["agy statusbar"]
Two things to understand about the env model. Setting env_allow switches that role from inherit-everything to an allowlist: baseline variables (PATH, HOME, TERM, SHELL, LANG, LC_*, XDG_* and friends) plus exactly what you list. Wildcards are prefix-only (GOOGLE_*), and env_deny is applied first, so a denial always beats an allowance.
input_prompts and chrome_markers are the escape hatch for non-Claude TUIs: the first teaches the readiness heuristic what “this agent is blocked on input” looks like, the second tells the sidebar which lines are chrome and should not show up in the activity preview.
Result: each API key exists only in the process that needs it. Your Gemini reviewer cannot see ANTHROPIC_API_KEY, and neither of them can see your cloud credentials.
Recipe 4: regulated, gateway-enforced#
This is the config for the case Choragos was actually built for: every prompt an agent sends must pass through a gateway inside your own trust boundary, and if the gateway is not there, no work happens.
[[roles]]
name = "orchestrator"
command = "claude"
model = "opus"
start = true
prompt_template = "You coordinate the team. Plan and delegate only; never implement, review, or audit yourself."
[[roles]]
name = "coder"
command = "claude"
model = "opus"
args = ["--strict-mcp-config"]
prompt_template = "Implement the requested change. Run the project's tests before reporting done."
[[roles]]
name = "release"
command = "claude"
model = "haiku"
args = ["--strict-mcp-config"]
prompt_template = "Run the release flow only after explicit human sign-off. Never push without approval."
approve = true # every delegation here waits for y/n
env_deny = ["AWS_*", "GCP_*"]
[sphragis]
enabled = true # explicit: require the gateway
fail_closed = true # refuse delegation while it is down
addr = "127.0.0.1:8787"
[pricing."claude-opus-4-8"]
input = 5.0
output = 25.0
cache_read = 0.5
cache_creation = 6.25
[pricing."claude-haiku-4-5"]
input = 1.0
output = 5.0
cache_read = 0.1
cache_creation = 1.25
Set enabled = true explicitly. The default is soft on purpose: if you never mention the gateway, the binary is not on PATH, and nothing is listening on addr, the deck starts with the gateway off and logs a warning rather than refusing to run. That is friendly for a first install and wrong for a regulated one. Writing the key is what turns the fail-closed guarantee on.
With fail_closed = true, dispatch refuses every command while the gateway is unhealthy and logs dispatch refused: gateway down. The one exempt verb is reload, on the reasoning that reloading changes the team, not the work.
Result: no agent in this session can reach a model provider except through a gateway you run, and the deck stops handing out work the moment that stops being true.
Recipe 5: long-running and unattended#
Sessions detach like tmux does. The problem with an unattended session is not the detaching, it is that a worker can get stuck in a loop at three in the morning and nothing tells you.
[[roles]]
name = "orchestrator"
command = "claude"
model = "opus"
start = true
prompt_template = "You coordinate the team. Plan and delegate only; never implement, review, or audit yourself."
[[roles]]
name = "coder"
command = "claude"
model = "opus"
args = ["--strict-mcp-config"]
prompt_template = "Implement the requested change. Run the project's tests before reporting done."
timeout = "45m" # wall clock per delegation
timeout_action = "restart" # SIGTERM the role; restart takes over
restart = "on-failure"
restart_retries = 3
[ui]
bell = true
# hooks inherit the `choragos serve` environment; keep webhook URLs out of this file
on_gate = 'curl -sf -d "$CHORAGOS_ROLE awaits approval: $CHORAGOS_TASK" "ntfy.sh/$NTFY_TOPIC"'
on_input = 'curl -sf -d "$CHORAGOS_ROLE is waiting for input" "ntfy.sh/$NTFY_TOPIC"'
on_timeout = 'curl -sf -d "$CHORAGOS_ROLE timed out on: $CHORAGOS_TASK" "ntfy.sh/$NTFY_TOPIC"'
The timeout clock starts when the task is delivered, which is after any approval gate, so a task sitting in your approval queue does not burn its budget. timeout_action = "notify" rings the bell, marks the board, fires the hook and leaves the worker running; restart kills the role and lets the restart budget respawn it. The restart_retries budget exists so a genuinely broken command cannot crash-loop, and a manual prefix+R resets it.
Then run it headless:
choragos serve --detach # double-forks, setsid, logs to .choragos/logs/server.log
choragos ls # sessions across projects
choragos attach # TUI back, with scrollback, board, gates and layout restored
choragos kill --all
prefix+d detaches from inside the deck. Only one client can attach at a time; a second choragos attach is refused and tells you which pid is holding it.
Result: the crew keeps working after you close the laptop lid, and your phone buzzes when it needs you.
Telemetry, and What report Gives You#
With the gateway in the path, each role is launched with ANTHROPIC_BASE_URL pointed at http://<addr>/agent/<role>. That per-role path suffix is the whole attribution mechanism: the gateway counts tokens per agent, and Choragos scrapes them back off the gateway’s Prometheus endpoint. One regex is the entire contract:
var tokenLine = regexp.MustCompile(
`^sphragis_tokens_total\{agent="([^"]*)",model="([^"]*)",direction="([^"]*)"\} ([0-9.eE+]+)$`)
Four directions are parsed (input, output, cache_creation, cache_read), the fetch has a one second timeout with the body capped at 1 MiB, and cumulative snapshots go into the event log every 30 seconds and on quit. Cost is just tokens * price_per_mtok / 1e6 against your [pricing] table, with the longest matching model-name prefix winning. Sidebar cards show ↑in ↓out $cost, and sub-cent amounts render to four decimals so a cheap run does not display as $0.00.
No SDK hooks, no vendor instrumentation. The gateway counts what the provider reports.
After a run, choragos report parses the event log and prints a per-role table:
run 2026-07-18 09:14:22 · wall 1h12m · /Users/nn/git/myservice
ROLE TASKS DONE BUSY AVG FIRST LAST TOKENS
orchestrator - - - - 09:14:31 10:26:04 184.2k
coder 6 6 52m18s 8m43s 09:16:02 10:21:37 1.4M
reviewer 6 5 31m04s 6m12s 09:23:11 10:24:50 612.9k
auditor 2 2 9m47s 4m53s 09:58:20 10:19:02 203.1k
1 task(s) never reported work-done
That last line is the one I look at first. A task that never reported back is either a worker that finished and forgot to call work-done, or one that quietly died. Both are worth knowing before you trust the diff.
Installation and Quick Start#
# macOS / Linux
brew install sphragis-oss/sphragis/choragos
# native macOS app with the CLI bundled
brew install --cask sphragis-oss/sphragis/choragos-desktop
# from source (Go 1.26+)
git clone https://github.com/sphragis-oss/choragos.git
cd choragos && make build
First run:
cd ~/git/your-project
choragos init --auto # detects the project, writes language-specific roles
choragos doctor # verifies binaries, socket path, git repo, gateway
choragos serve
Want to see the deck before you point real agents at it? make demo runs five placeholder panes with the gateway off, using examples/demo.toml, where each role is a plain sh -c that prints a banner and exec cat. It is the cheapest way to learn the keymap.
The deck is driven tmux-style behind a prefix key (default ctrl+b):
| Binding | Action |
|---|---|
prefix + v / - | Split vertical / horizontal |
prefix + h j k l | Move focus; 1..9 jumps to a role directly |
prefix + z | Zoom the focused tile |
prefix + t | Task board |
prefix + R | Restart the focused role |
prefix + p | Pause / resume a role (SIGSTOP / SIGCONT) |
prefix + a | Broadcast input to all agents |
prefix + C | Reload the config |
prefix + d | Detach |
prefix + / | Search scrollback |
ctrl+g | Toggle gateway enforcement live |
Closing a tile never kills its agent. That one took me a while to stop flinching at.
prefix + p is the underrated one: freezing a role mid-flight lets you read what it is doing without it racing ahead, and resuming keeps the agent’s context intact. It is best-effort, though, and the docs say so: children in their own process groups keep running, and an API call in flight during a long pause may drop and retry on resume.
Editing the Team Without Restarting#
Edit .choragos.toml, then choragos reload or prefix+C. The deck re-reads the file and converges the running team by role name: added roles spawn and get their boot brief, removed roles stop gracefully, a changed command/args/model/env_* respawns that role, and changed prompt_template/approve/restart* apply on the next task without a restart.
The guardrails matter more than the feature:
- The start role’s process is never touched. Its spec changes and removal are ignored until a deck restart, since killing the orchestrator mid-session loses the plan.
- A role with a pending gate or an unresolved delegation is not respawned. Resolve the work, reload again.
- On the built-in team (no config file) there is nothing to re-read, so reload is refused.
- Only
[[roles]]converges live.[keys],[ui]and[sphragis]changes need a restart.
Troubleshooting#
| Issue | Symptom | Resolution |
|---|---|---|
| Role will not start | command not found at boot | command must be a real binary on PATH; shell aliases do not resolve. Run choragos doctor |
| Socket path too long | doctor fails the socket check | macOS caps sun_path near 104 bytes. Set CHORAGOS_SOCK to a short path, or work from a shorter directory |
| Delegation refused | dispatch refused: gateway down in events.log | The gateway is unhealthy and fail_closed = true. Start Sphragis, or toggle enforcement with ctrl+g |
| Config key ignored | Startup warning in events.log | Unknown key, usually a typo. Warnings are never silent, so read them |
| Scrollback looks empty | A full-screen agent TUI shows only its last frame | Expected. Full-screen TUIs repaint in place and never emit past content to the PTY, so the history was never in the byte stream |
| Token counts reset | Cards drop to zero mid-session | The gateway restarted. Counters are the gateway’s, not the deck’s |
| Second attach refused | Error naming a pid | One client at a time by design. Kill the holder or detach it |
That transcript one deserves a sentence, because it looks like a bug and is not. If an agent runs a full-screen TUI, its “transcript” is only whatever was last on screen, however long the session ran. That is not capture loss. The narrative never existed in the byte stream to capture.
Choragos: Pros and Cons#
Pros#
| Advantage | Description |
|---|---|
| Real readiness detection | Owning and parsing the PTY removes the boot races that make multiplexer scripting flaky |
| Vendor-neutral roles | Any binary on PATH is a valid seat; mix Claude and Gemini in one team |
| Least privilege per seat | env_allow / env_deny keep credentials out of roles that have no business holding them |
| Undo with the right blast radius | Checkpoints are per-delegation and never touch HEAD, the index, or history |
| Cost you can see | Live per-role tokens and dollars, plus a post-run report, without any SDK integration |
| Compliance path is real | Fail-closed gateway enforcement, not a config comment nobody checks |
| Small dependency surface | Eight direct Go dependencies, ~6.4k lines of production Go against ~5.6k lines of tests, 180 tests, CI with race detector, govulncheck, CodeQL, zizmor and SHA-pinned actions |
Cons#
| Limitation | Description |
|---|---|
| Terminal-first | The desktop app exists for macOS, but the mental model is still panes and a prefix key |
| Anthropic-shaped gateway path | Per-role routing works through ANTHROPIC_BASE_URL, so a non-Anthropic CLI is a first-class role but not a first-class gateway client |
| One attached client | No multi-client attach, no remote attach, no live migration across version skew |
| Reload is partial | Only [[roles]] converges live; keys, UI and gateway settings need a restart |
| Checkpoints need git | In a non-git directory the deck warns and runs unprotected |
| Pause is best-effort | SIGSTOP does not reach children in their own process groups |
| Young project | v0.11.2, single maintainer, API and config surface still moving |
When to Use It, and When Not#
Use Choragos when:
- You are already running two or more agents at once and hand-carrying context between them
- You want a different vendor or model per seat, especially a reviewer that is not the same model as the coder
- Different seats should have different credentials
- You need a record of what agents did, what it cost, and a way to undo one task cleanly
- You are in an environment where every prompt must transit a gateway you control
Do not use Choragos when:
- One agent in one window is genuinely enough. It usually is. Do not add an orchestrator to a job a single agent finishes in ten minutes
- You want unattended, fully autonomous runs with no human in the loop. Gates and approvals are the point here, not a compromise
- You need a hosted, multi-user control plane. This is a local binary for one operator
Try It#
The project is Apache 2.0 on GitHub, with the docs I found most useful linked below:
- sphragis-oss/choragos - the repo, v0.11.2
- Configuration reference - every
.choragos.tomlkey - Building your own team - roles, briefs, and the context-hygiene numbers
- Control protocol - the delegate/work-done wire contract if you want to drive it from your own tooling
- Checkpoints design - why gates alone are not enough
brew install sphragis-oss/sphragis/choragos
cd ~/git/your-project
choragos init --auto && choragos doctor && choragos serve
Issues and PRs are welcome. The project requires DCO sign-off on every commit.
Conclusion#
The reason multi-agent setups fall apart is not that the models are not good enough. It is that nobody is deciding who does what, and nothing carries the result back, so a human becomes the coordination layer, and humans are slow, lossy, and asleep at 03:00.
Choragos answers that with a team rather than a tool. Declare the seats and let one of them hold the plan. Give them two verbs and a socket, so handoff is a command rather than a copy-paste, and every task has an ID you can point at. Give each seat its own context and only the credentials it needs, so a review is independent and a reviewer cannot touch production. Gate the seats that can do damage, and snapshot before every task, so a bad delegation costs you one rollback instead of an afternoon. Owning the PTY is what makes all of that land reliably, but it is the plumbing, not the point.
The compliance side is the part I care about most, and it is the reason this project lives in the same org as Sphragis. If your agents are going to send prompts about your codebase to a model provider, that traffic should pass through something you run, and the orchestrator should refuse to hand out work when it does not. fail_closed = true is four words that turn a policy document into an enforced behaviour.
Start with choragos init and the default trio. Add the gateway when you need the audit trail, add approve = true on the seats that can do damage, and add timeouts before you first detach and walk away. That order has worked well for me.
If you found this useful, you might also enjoy my related posts:
- Sphragis - The EU AI Act Compliance Gateway You Actually Control
- Building a Kubernetes Agent with Google ADK for Go
- Shipping an Obsidian Plugin With a Coding Agent

