Collaborative OpenCode session sharing. Pair-program a live AI session from another OpenCode instance in real time.
- Session sharing — host runs
/chorus-shareand sends a join command to a collaborator - Pair programming — joiners with
edit(default) can send prompts into the host’s LLM session - Live chat —
/chorus-chatside channel via temporary TUI toasts (does not pollute session history) - Cloud backup — optional archive of session events to S3/R2
Chorus is an OpenCode plugin, a VS Code adapter, and a Rust WebSocket relay (chorus-relay). The relay is the core: adapters are thin clients on the same /host + /ws wire protocol (@chorus/shared).
Host adapter (OpenCode or VS Code) → spawns chorus-relay + issues join token via /host
Joiner adapter (OpenCode or VS Code) → connects to relay /ws
Joiner prompts → relay → host control channel → host session (OpenCode) or host panel (VS Code)
OpenCode ↔ OpenCode is the primary path (full LLM loop + transcript mirror). VS Code uses the same relay: it can host a session that terminal/OpenCode joiners connect to, or join a terminal/OpenCode host. VS Code does not drive OpenCode’s model when sharing — publish host/AI lines manually or pair with an OpenCode host. See packages/vscode/README.md.
The host machine needs the chorus-relay binary on PATH or built in-repo (target/release/chorus-relay). Override with CHORUS_RELAY_BIN.
Until packages are published to npm, install from this repo:
# Build the Rust relay
cargo build -p chorus-relay --release
# Link the plugin into your OpenCode project
mkdir -p .opencode/plugin
bun add --cwd .opencode/plugin /path/to/chorus/packages/pluginThen add to OpenCode config:
{
"plugin": ["@chorus/plugin"]
}cargo build -p chorus-relay --release
bun install
bun run build:tsIn VS Code: Extensions: Install from Location… → packages/vscode, then Chorus: Share Session / Join Session. Details: packages/vscode/README.md.
One monorepo, two ecosystems, one wire contract:
| Path | Artifact | Description |
|---|---|---|
packages/plugin |
npm @chorus/plugin |
OpenCode plugin — tools, hooks, spawns/manages relay |
packages/client |
npm @chorus/client |
Shared JoinClient + RelayServer for host adapters |
packages/vscode |
VS Code extension chorus |
Share/join Chorus sessions from VS Code |
packages/shared |
npm @chorus/shared |
TypeScript types + codecs for joiner and host-control protocols |
crates/chorus-relay |
chorus-relay binary |
Rust WebSocket relay (/ws joiners, /host control plane) |
protocol/ |
fixtures (not published) | Canonical JSON examples both TS and Rust must deserialize |
Root package.json scripts are the only task entry (build, test, typecheck). Bun workspaces own packages/*; Cargo owns crates/*.
bun install
bun run build # release relay + TS packages
bun run test # relay tests + TS/Bun tests (includes protocol fixtures)
bun run typecheck
cargo test -p chorus-relayRun several isolated OpenCode servers on one machine (each with Chorus loaded) and drive share/join over the HTTP API:
# Requires `opencode` on PATH (https://opencode.ai/docs)
bun run multi-agent -- setup --agents 3
bun run multi-agent -- up --agents 3
bun run multi-agent -- smoke # health + chorus tools registered
bun run multi-agent -- pair # /chorus-share then /chorus-join
bun run multi-agent -- down
# Protocol-only stress (no OpenCode): N concurrent JoinClients
bun run test:relay-stressSee bun run multi-agent -- help for ports/env (OPENCODE_BASE_PORT, CHORUS_BASE_PORT, OPENCODE_BIN).
Run chorus-relay on your machine and OpenCode agents in containers (different published ports). Containers reach the relay via host.docker.internal.
bun run build # release relay + plugin dist (needed for image)
bun run docker-agents -- up --agents 2
bun run docker-agents -- smoke
bun run docker-agents -- pair # /chorus-share then /chorus-join across containers
bun run docker-agents -- downup starts the host relay on 0.0.0.0:7742, builds chorus-opencode-agent:local, and publishes agents on 4100+. Override with CHORUS_PORT, OPENCODE_BASE_PORT, CHORUS_HOST_TOKEN, OPENCODE_VERSION.
For live mirrored context on joiners, open the web UI (not only opencode attach):
- Host: http://127.0.0.1:4100
- Joiner: http://127.0.0.1:4101
opencode attach works for driving the session, but OpenCode’s attach TUI often does not live-render plugin-injected transcript lines.
Protocol-level tests for the shared @chorus/client stack (same code paths as the OpenCode plugin and VS Code extension):
bun run test:vscode-e2e # VS Code adapter: email gate, pending approve, collab.input
bun run test:vscode-relay-e2e # cross-adapter: VS Code host ↔ terminal joiner (both directions)
bun run test:adapters-e2e # both of the above
bun run test:security-e2e # OpenCode host + protocol joiner (requires multi-agent up)
bun run test:network-e2e # CIDR / source-port allowlist on chorus-relayThree-environment relay test (test:vscode-relay-e2e): models host vscode, joiner vscode, and a disallowed joiner. Run all scenarios locally (default), or split across machines with CHORUS_E2E_ROLE:
| Env | Role | Command |
|---|---|---|
| env1 | host vscode | CHORUS_E2E_ROLE=host-vscode bun scripts/vscode-relay-e2e.ts |
| env2 | joiner vscode | CHORUS_E2E_ROLE=joiner-vscode bun scripts/vscode-relay-e2e.ts |
| env3 | disallowed joiner | CHORUS_E2E_ROLE=disallowed-joiner bun scripts/vscode-relay-e2e.ts |
Coordination file: CHORUS_E2E_STATE (default /tmp/chorus-vscode-relay-e2e.json).
Chorus reads a layered config file plus environment variables.
Search order (later layers override earlier keys):
- Built-in defaults
- System / org:
/etc/chorus/config.json(orCHORUS_SYSTEM_CONFIG) - User:
~/.config/chorus/config.json - Project:
./chorus.jsonor./.chorus/config.json - Explicit file:
CHORUS_CONFIG=/path/to/config.json - Environment variables (ops overrides for port, public host, backup)
Copy chorus.example.json to get started. Example enterprise lock:
{
"org": { "name": "Acme Engineering" },
"security": {
"requireApproval": true,
"allowSkipApproval": false,
"requireRepoMatch": true,
"requireEmailDomainMatch": true,
"allowedEmailDomain": "acme.com",
"additionalRepoRemotePrefixes": ["git://"],
"repoRemoteRewrites": [{ "from": "github.acme.com", "to": "github.com" }],
"defaultRole": "edit",
"tokenTtlMs": 86400000
},
"relay": {
"allowOpenBind": false,
"allowedCidrs": ["10.0.0.0/8", "100.64.0.0/10"]
}
}These knobs are pairing controls, not a complete enterprise ACL: email and git remote are self-asserted on join, and later config layers (user/project) can still override /etc/chorus/config.json. See docs/ENTERPRISE.md for the gaps a security review will hit. For VPN / AWS VPC / Azure VNet / GCP VPC patterns, see docs/NETWORK.md.
| Field | Default | Description |
|---|---|---|
security.requireApproval |
false |
Joiners pending until host approve/deny |
security.allowSkipApproval |
true |
If false, tool args cannot turn approval off |
security.requireRepoMatch |
false |
If true, share fails without a git origin |
security.requireEmailDomainMatch |
false |
If true, share fails unless allowedEmailDomain is set; joiners must use that domain |
security.allowedEmailDomain |
— | Company email domain (e.g. acme.com) enforced on join |
security.additionalRepoRemotePrefixes |
[] |
Extra git URL prefixes to strip when matching remotes (e.g. git://) |
security.repoRemoteRewrites |
[] |
Host substitutions after normalize (e.g. { "from": "github.acme.com", "to": "github.com" }) |
security.defaultRole |
edit |
Role when /chorus-share omits role |
security.tokenTtlMs |
— | Optional join-token TTL |
relay.port / relay.publicHost |
— | Relay listen / advertised join host |
relay.bind |
0.0.0.0 |
Listen address (CHORUS_BIND) |
relay.allowedCidrs |
[] |
CIDR/IP allowlist; empty = unrestricted |
relay.deniedCidrs |
[] |
Explicit deny CIDRs (deny wins) |
relay.allowedPorts |
[] |
Peer source-port allowlist (single-machine e2e) |
relay.allowOpenBind |
true |
If false, refuse bind to 0.0.0.0 / :: |
relay.allowLoopback |
true |
Admit loopback IP when an allow-CIDR list is set |
backup.bucket / region / endpoint |
— | Optional S3/R2 backup |
org.name / org.policyNote |
— | Shown in share + status |
/chorus-status prints the effective config and which files contributed.
| Env var | Default | Description |
|---|---|---|
CHORUS_CONFIG |
— | Absolute path to a config JSON file (highest file layer) |
CHORUS_SYSTEM_CONFIG |
/etc/chorus/config.json |
Org-wide config path |
CHORUS_USER_CONFIG |
~/.config/chorus/config.json |
Per-user config path override |
CHORUS_PORT |
7742 |
Relay listen port (overrides file) |
CHORUS_BIND |
0.0.0.0 |
Relay bind address |
CHORUS_ALLOWED_CIDRS |
— | Comma-separated CIDR/IP allowlist |
CHORUS_DENIED_CIDRS |
— | Comma-separated CIDR/IP denylist (deny wins) |
CHORUS_ALLOWED_PORTS |
— | Comma-separated peer source ports |
CHORUS_ALLOW_OPEN_BIND |
true |
Set false to refuse 0.0.0.0 / :: |
CHORUS_ALLOW_LOOPBACK |
true |
Admit loopback when allowlist is set |
CHORUS_RELAY_BIN |
auto-detect | Path to chorus-relay binary |
CHORUS_HOST_TOKEN |
random | Host control secret (set by plugin when spawning) |
CHORUS_RELAY_HOST |
127.0.0.1 |
Relay host to attach to (e.g. host.docker.internal:7742) |
CHORUS_PUBLIC_HOST |
local IP:port | Host:port advertised in /chorus-share join URLs |
CHORUS_EXTERNAL_RELAY |
— | 1 to attach to an existing relay (no spawn/kill) |
CHORUS_AWS_BUCKET |
— | S3/R2 bucket for session backup |
CHORUS_AWS_REGION |
us-east-1 |
AWS region |
CHORUS_AWS_ENDPOINT |
— | Custom endpoint (for R2/MinIO) |
| Control | Behavior |
|---|---|
| Join token | Still required; issued by /chorus-share with a role |
| Display name | Required on /chorus-join — empty names are rejected |
| Host approval | Config security.requireApproval (default off); when on, joiners appear in a live numbered queue — /chorus-approve 1 (full userId still works) |
| Git repo gate | If the host share directory has origin (or requireRepoMatch), joiners must present the same remote. Extra prefixes/rewrites come from additionalRepoRemotePrefixes / repoRemoteRewrites. |
| Company email gate | If allowedEmailDomain is set (or requireEmailDomainMatch), joiners must auth with an email at that domain |
| Network allowlist | If relay.allowedCidrs is set, only those TCP peers reach /ws / /host (see docs/NETWORK.md) |
| Kick | /chorus-kick <userId> disconnects an active joiner |
Remote tunneling (bore / cloudflared) is not implemented yet — share a LAN IP + port for now.
- docs/STATUS.md — what’s done and what still needs to happen
- docs/DECISIONS.md — OpenCode plugin vs from-scratch, languages, license
- docs/ENTERPRISE.md — security gaps for enterprise use (claims vs proofs, policy floor, SSO, audit)
- docs/NETWORK.md — CIDR allowlist, VPN, AWS VPC / Azure VNet / GCP VPC
- docs/THIRD_PARTY_LICENSES.md — third-party license audit for commercial use
MIT