Operate the Chorus daemon
Use this guide after you have verified a foreground daemon connection. The commands apply to the supported always-on backends: Claude Code, Codex, Kiro, and Pi.
Hermes stays online through its own gateway, not the Chorus daemon. It supports comment-based command approvals and interrupt/resume; research and idea-creation operation turns are not supported by its gateway.
Upgrade daemon backends to 0.20.0
Section titled “Upgrade daemon backends to 0.20.0”Update the Chorus CLI using the upgrade command, then restart the daemon when ongoing work allows. Host CLIs are updated separately. Check these backend-specific requirements before the daemon first starts work:
- Codex App Server: verified with
codex-cli 0.157.1; daemon
argsaccept only model options and permitted config overrides. In--chorus-onlymode, verify the required MCP tool allowances in Codex configuration. - Claude Code stream-json: verified with Claude Code 2.1.283 and 2.1.284; interruption first uses the protocol, and background questions must use Chorus comments or elaboration.
- Pi RPC: requires Pi 0.85.0 or newer
(verified with 0.85.1);
--pluginsrefreshes the Chorus packages but not Pi itself.
Choose what the daemon can access
Section titled “Choose what the daemon can access”Each --cwd registers one working directory as a separate target in Chorus:
chorus daemon \ --agent claude-code \ --cwd ~/work/project-a \ --cwd ~/work/project-b \ --chorus-onlyWith no --cwd, Chorus uses the directory where you start the command. Use repeatable
--browse-root options only when users need to choose directories below a broader root.
A browse root allows discovery; it does not register every directory as an online
connection.
When you fix a working directory for a project’s agent, that
path must be one the daemon serves — include it in this daemon’s cwds set so the fixed
directory resolves to an online connection.
Configure daemon.json
Section titled “Configure daemon.json”The daemon reads its settings from ~/.chorus/daemon.json. In the common single-agent case
it is one flat JSON object holding the agent credential and the runtime options; to serve
several agents from one daemon you instead list them under an agents array — see
Run several agents in one daemon. Either way it is
written atomically with 0600 (owner read/write only), so a crash mid-write never leaves a
truncated file. Set CHORUS_DAEMON_CONFIG_PATH to use a different file. If you use this
override, set it consistently for both the CLI and the daemon service; an export in your
shell does not change an already-running service.
Fields
Section titled “Fields”| Field | Type | Meaning |
|---|---|---|
url | string | Chorus server URL. |
apiKey | string | Agent API key (cho_…). |
agentUuid | string | Authenticated agent UUID (informational). |
agentName | string | Authenticated agent name (informational). |
cwds | string[] | Working directories this daemon serves. Each path is one independent online connection. |
browseRoots | string[] | Roots exposed to remote working-directory discovery. A browse root does not create a connection. |
agent | string | Local backend to wake: "claude-code", "codex", "kiro", "pi", or "dsh". |
sigintTimeoutMs | number | How long (ms) to wait after an interrupt before forcing the process to stop; Claude Code, Codex, and Pi send a protocol interrupt first. Default 10000. |
agents | object[] | Optional. Serve several independent agents from one daemon; each entry can override shared runtime defaults, but args and env are never inherited. See Run several agents in one daemon. |
A minimal file after chorus login plus chorus daemon install:
{ "url": "https://chorus.example.com", "apiKey": "cho_REDACTED", "agentUuid": "8a1c…", "agentName": "Build Agent", "cwds": ["/home/demo/work/project-a"], "browseRoots": ["/home/demo/work"], "agent": "claude-code", "sigintTimeoutMs": 10000}How the file is created
Section titled “How the file is created”chorus agents addis the usual way the file is first created: it validates each configured agent’s credential and writes it as an entry in theagentsarray. See Connect an agent runtime.chorus loginvalidates your URL and key, then writesurl,apiKey,agentUuid, andagentNamefor a single flat agent.chorus daemon installadditionally writescwds,browseRoots, andagent(it prompts for the served directories and backend when they are not already set).- A first
chorus daemonrun on a terminal completes any missing credentials interactively and writes them.
How the file is updated
Section titled “How the file is updated”Every writer performs a shallow merge: the new fields are merged over whatever is
already on disk, and unrelated fields are preserved. Re-running chorus login refreshes
the credentials without clearing your cwds, agent, or sigintTimeoutMs; re-running
chorus daemon install updates the served set without discarding the credential. A
missing or corrupt file is treated as an empty object, so a re-login always produces a
valid file rather than failing.
Field, flag, and environment variable
Section titled “Field, flag, and environment variable”The runtime options below can be supplied in three ways. Precedence is flag, then environment
variable, then daemon.json, so a one-off flag or environment override never has to be
written to the file:
| Concern | daemon.json field | CLI flag | Environment variable |
|---|---|---|---|
| Server URL | url | --url | CHORUS_URL |
| API key | apiKey | --api-key | CHORUS_API_KEY |
| Working directories | cwds | --cwd (repeatable) | CHORUS_DAEMON_CWDS |
| Browse roots | browseRoots | --browse-root (repeatable) | CHORUS_DAEMON_BROWSE_ROOTS |
| Backend | agent | --agent | CHORUS_AGENT |
| SIGINT grace | sigintTimeoutMs | --sigint-timeout | CHORUS_DAEMON_SIGINT_TIMEOUT |
| Permission mode | (not persisted) | --yolo / --chorus-only | CHORUS_YOLO / CHORUS_CHORUS_ONLY |
The permission mode is deliberately not stored in daemon.json. Pass --chorus-only
(or set CHORUS_CHORUS_ONLY=1) at start time; the installed service captures
--chorus-only in its unit, not in the file.
Run several agents in one daemon
Section titled “Run several agents in one daemon”One chorus daemon process can serve several fully independent agents at once — different
personas, permissions, accounts, or even different backends — instead of running a separate
daemon per agent. List them under an agents array; each entry is one agent:
{ "url": "https://chorus.example.com", "sigintTimeoutMs": 8000, "agents": [ { "apiKey": "cho_alpha", "agentType": "claude-code", "cwds": ["/home/demo/project-a"] }, { "apiKey": "cho_beta", "agentType": "kiro", "cwds": ["/home/demo/project-b"], "permissionMode": "chorus" } ]}Shared runtime settings such as url and sigintTimeoutMs provide defaults, and an agent’s
own value overrides them. args and env are per-agent only, never shared defaults.
Each agent gets its own identity (from its key), its own connections (one for each of its
cwds), its own wake queue, and its own backend. Agents are therefore woken and run
independently, and one agent’s failure never disrupts the others. On the server each appears as its own connection
in Settings → Agents, keyed on agent, host, and directory. Agents may even share a working
directory; the daemon does not serialize them, so avoid concurrent conflicting work in one git
tree (use separate branches or worktrees).
Per-agent fields
Section titled “Per-agent fields”| Field | Meaning |
|---|---|
apiKey (required) | The agent’s cho_ key — determines its identity. |
url | Chorus server (may differ per agent — a different server or workspace). |
agentType | claude-code, codex, kiro, or pi (backends may be mixed). |
cwds | Working directories this agent serves (one connection each). |
permissionMode | yolo or chorus. |
maxConcurrency | This agent’s own wake-concurrency cap (default 4). |
sigintTimeoutMs | How long (ms) to wait after an interrupt before forcing the process to stop. |
browseRoots | Directory-discovery allowlist. |
args | Optional string array of extra backend CLI arguments; default []. See Set an agent’s model, thinking level, and environment. |
env | Optional object mapping environment names to string values; default {}. |
Set an agent’s model, thinking level, and environment
Section titled “Set an agent’s model, thinking level, and environment”Add the fields to the intended entry in ~/.chorus/daemon.json. This Pi example uses
illustrative credentials and a non-secret environment value; replace the URL, key, and
model with values valid for your setup. Merge it into your existing file rather than
overwriting other agents:
{ "url": "https://chorus.example.com", "agents": [ { "agentName": "pi-worker", "apiKey": "cho_REDACTED", "agentType": "pi", "args": ["--model", "anthropic/claude-sonnet-4-6", "--thinking", "high"], "env": { "PROVIDER_REGION": "us-east-1" } } ]}The model is an example, not a Chorus default. Use flags and values supported by your
backend; there are no separate model or thinking fields. Both daemon wakes and
chorus agents run --name pi-worker use this entry. offline agents stay non-wakeable.
- No global inheritance: with a nonempty
agentsarray, top-levelargsorenvcause an error. Move them into each intended entry. Other agents and your shell are unaffected. - Literal values: each
argselement is one token, without extra shell quoting.$HOME,${TOKEN}, and shell commands are not expanded. Chorus adds no dotenv loading, interpolation, secret lookup, or special handling for env values.envoverrides the child’s inherited environment; Chorus-managed controls still take precedence. Windows env names are case-insensitive; POSIX names are case-sensitive. - Validation:
null, non-string tokens/values, NUL characters, and env names outside[A-Za-z_][A-Za-z0-9_]*are rejected before launch. Empty string values are valid. Use options, not positional prompts or subcommands. For an unknown option, use"--future=value"if the backend supports it;"--future", "value"is ambiguous and rejected. - Protected controls: persistent args cannot change known session/resume, prompt,
transport/output, cwd, managed MCP, or permission controls. Help, version, and other options
that only print information and exit are also forbidden, as are a bare
--and the stdin marker-. AllCHORUS_*env names and the nested-Claude context namesCLAUDECODE/CLAUDE_CODE_ENTRYPOINTare reserved, regardless of case. Configuring any of them causes an error rather than being silently discarded. Use dedicated Chorus settings or explicit foreground passthrough instead. - Windows command shims:
.cmd/.batlaunches reject configured empty tokens and tokens containing whitespace, quotes, or command metacharacters. Use a native executable for those values; explicit foreground passthrough is not guaranteed shell-safe through a shim.
env can contain plaintext provider credentials. Protect the file
(chmod 600 ~/.chorus/daemon.json on POSIX) and never commit secrets. Chorus omits configured
values from validation and launch messages, but the child program or OS inspection may expose
them; there is no encryption or vault.
If you set a custom home such as CODEX_HOME or PI_CODING_AGENT_DIR, configure the Chorus
integration there too.
Apply changes: run chorus daemon restart. The daemon reads these settings at startup,
not on each wake; editing the file changes neither running children nor later wakes until
restart. Each new chorus agents run rereads the profile, without changing an already-running
child. Remove args/env to restore the uncustomized behavior.
Legacy flat files: without a nonempty agents array, top-level args/env apply to the
sole daemon agent. Foreground selection requires agents[]. To share settings, move the
flat URL, key, identity, args, and env into an entry, renaming agent to agentType.
Adding an agent with chorus agents add or chorus login --add also moves an existing flat
credential profile and its args/env into agents[0], removing those top-level keys. An
incomplete flat profile without its key must be completed or converted manually first.
See the CLI reference for exact one-off override and subcommand rules.
Add another agent
Section titled “Add another agent”chorus login --addvalidates a new key and appends it as another agent. The first--addon a flat file migrates the existing credential intoagents[0]and adds the new key asagents[1]; a duplicate key is refused, and an existing agent is never overwritten.chorus daemon install --addloops the install wizard so you can add several agents in one pass (terminal only).- Hand-editing
~/.chorus/daemon.jsonis always supported.
Restart the daemon after editing the file (chorus daemon restart), then confirm each agent
appears in Settings → Agents.
How each backend receives its key
Section titled “How each backend receives its key”Each agent authenticates with its own key, but how that key reaches the woken subprocess depends on the backend:
- Claude Code — automatic. The daemon writes a per-wake MCP config carrying that agent’s URL and key. Nothing to configure.
- Kiro — automatic, via the environment. The installed
mcp.jsonreferences${CHORUS_URL}and${env:CHORUS_API_KEY}, which the daemon exports per wake, so each Kiro agent authenticates with its own key. - Codex — automatic, via the environment.
chorus agents addconfigures Codex without storing the key in its config: it writesCHORUS_URL/CHORUS_API_KEY/CHORUS_AGENT_PROFILEinto~/.codex/.env(which Codex loads at startup) and setsbearer_token_env_var = "CHORUS_API_KEY"inconfig.toml, so Codex reads its key from the environment rather than from the file. The daemon injects each agent’s own key per wake, so several Codex agents in one daemon each authenticate as themselves. - Pi — automatic, via the environment. The daemon exports each agent’s
CHORUS_URL/CHORUS_API_KEY/CHORUS_AGENT_PROFILEinto the woken Pi session, which thechorus-piextension reads, so each Pi agent authenticates with its own key.
Install the Linux service
Section titled “Install the Linux service”On Linux, install the verified configuration as a user service:
chorus daemon install \ --agent claude-code \ --cwd ~/work/project-a \ --chorus-onlyThe installer starts the service immediately and configures it to start when the user
logs in. Run chorus agents add (or chorus login) first so the service can read the
saved credential — or pass --daemon-autostart to chorus agents add to install this
service in the same step.
Provide credentials a woken agent needs
Section titled “Provide credentials a woken agent needs”The daemon supplies the Chorus connection (CHORUS_URL, CHORUS_API_KEY, and
CHORUS_AGENT_PROFILE), but not model-provider credentials. If your backend reads provider
credentials from environment variables, supply them in the agent’s env or in the daemon’s
inherited environment. A systemd --user service does not inherit exports from your login
shell; to supply them through the service, add a drop-in and restart:
[Service]Environment=EXAMPLE_PROVIDER_API_KEY=…systemctl --user daemon-reload && systemctl --user restart chorus-daemonBackends that read provider auth from a file instead (for example Claude Code and Codex under
~/.claude / ~/.codex) only need the daemon’s HOME set correctly, which the installed unit
already does.
Use these commands for routine operation:
chorus daemon statuschorus daemon logschorus daemon restartchorus daemon stopchorus daemon uninstallAfter changing credentials, working directories, backend, or permission mode, reinstall or restart the service and verify the connection in Settings → Agents.
Keep the Linux daemon running after logout
Section titled “Keep the Linux daemon running after logout”From Chorus 0.21.0, chorus daemon install tries to enable systemd lingering for the current user, so the user service survives the last logout and starts at boot without a login. The authorization attempt is non-interactive and time-bounded; it never waits for a password dialog.
If authorization is denied or logind is unavailable, service installation can still succeed, but Chorus warns that logout/boot persistence is not assured. Run chorus daemon status to check for linger: yes. Ask an administrator to run the printed sudo loginctl enable-linger <user> command if your host policy allows it.
Rerun chorus daemon install to repair an existing host. chorus agents add also checks lingering when installing the service or finding an existing service, even without --daemon-autostart or after declining auto-start. Pass --no-linger to either command to skip the attempt; it does not disable an existing setting. Without lingering, the daemon may stop after logout and not start until the next login. Uninstalling the Chorus service leaves lingering enabled because other user services may need it.
Run without a service
Section titled “Run without a service”For a short-lived connection, keep chorus daemon in the foreground. Detached mode is
also available:
chorus daemon -d --agent codex --cwd ~/work/project-a --chorus-onlyManage it with the same status, logs, restart, and stop commands. Do not combine
detached mode with a separately written process supervisor.
Check operating health
Section titled “Check operating health”Use both command-line and web status:
- Run
chorus daemon status. - Review recent output with
chorus daemon logs. - In Settings → Agents, confirm each expected working directory is Online.
- Start a small test session and confirm it targets the intended directory.
Logs can contain paths, prompts, and command output. Remove sensitive values before sharing them. For failures, continue with Troubleshoot agent connections.
Claude background-agent wait limit (0.22.0)
Section titled “Claude background-agent wait limit (0.22.0)”Claude daemon wakes now default to waiting 60 minutes after the main turn ends for unfinished background agents. This is a post-turn waiting ceiling, not a time limit for the entire wake, and not the interrupt grace period. Interactive Claude sessions are not changed. Update the Chorus CLI and Claude plugin, then restart the daemon for new wakes.
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: Native Claude value, passed through unchanged when explicitly set. Highest priority, including0or an empty string.CHORUS_CLAUDE_BG_WAIT_CEILING_MS: Used only when the native variable is absent. A non-negative safe integer in milliseconds; whitespace is trimmed.- Default:
3600000ms (60 minutes) when the native variable is absent and the Chorus override is unset or invalid. A nonempty invalid Chorus override logs a warning; empty or whitespace uses the default.
Priority is native → Chorus override → 3600000. Explicit 0 requests indefinite
waiting and can leave a stuck child holding the wake open forever. An empty native value
is still passed through; do not use it to request the default, because Claude may treat
it as unset and restore its own shorter limit. Remove the native variable entirely to
use the Chorus setting. Native-value validation and interpretation belong to Claude.
Set the Chorus override in the intended agent’s env object in
~/.chorus/daemon.json, for example:
{ "env": { "CHORUS_CLAUDE_BG_WAIT_CEILING_MS": "3600000" }}Alternatively configure the daemon service’s environment; a login-shell export does not
reach an already running service. Restart with chorus daemon restart. Check for an
inherited native value before expecting the Chorus override to take effect.
The parent agent must collect completion or explicitly stop its background children
before ending its turn. Increasing the ceiling does not replace this responsibility.
If Claude reports Background tasks still running after …s; terminating., Chorus
records an abnormal wake even when the raw process exit is zero, and attempts to post
an explanation on the triggering idea or task. Explicit interrupt/shutdown remains an
interruption, not a crash. No task status is repaired automatically and no new automatic
retry is added. Inspect partial work before resuming; if the comment cannot be posted,
the failure remains in the wake diagnostics.
Do not confuse backend limits: CHORUS_DSH_TIMEOUT_MS bounds the whole dsh wake,
with a default of 1800000 ms (30 minutes). It accepts positive safe integers; 0 or
invalid values fall back to that default, not infinite waiting. Claude’s post-turn setting
above does not apply to Codex, Pi, Kiro, or Hermes. Background-child lifetime in other
runtimes must not be inferred from Claude’s behavior.