Skip to content

Repository files navigation

chorus

Collaborative OpenCode session sharing. Pair-program a live AI session from another OpenCode instance in real time.

What it does

  • Session sharing — host runs /chorus-share and 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-chat side channel via temporary TUI toasts (does not pollute session history)
  • Cloud backup — optional archive of session events to S3/R2

How it works

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.

Installation

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/plugin

Then add to OpenCode config:

{
  "plugin": ["@chorus/plugin"]
}

VS Code adapter

cargo build -p chorus-relay --release
bun install
bun run build:ts

In VS Code: Extensions: Install from Location…packages/vscode, then Chorus: Share Session / Join Session. Details: packages/vscode/README.md.

Layout

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/*.

Development

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-relay

Multi-agent local testing

Run 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-stress

See bun run multi-agent -- help for ports/env (OPENCODE_BASE_PORT, CHORUS_BASE_PORT, OPENCODE_BIN).

Docker agents + host relay

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 -- down

up 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):

opencode attach works for driving the session, but OpenCode’s attach TUI often does not live-render plugin-injected transcript lines.

Adapter & relay e2e (no GUI)

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-relay

Three-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).

Configuration

Chorus reads a layered config file plus environment variables.

Config file (chorus.json)

Search order (later layers override earlier keys):

  1. Built-in defaults
  2. System / org: /etc/chorus/config.json (or CHORUS_SYSTEM_CONFIG)
  3. User: ~/.config/chorus/config.json
  4. Project: ./chorus.json or ./.chorus/config.json
  5. Explicit file: CHORUS_CONFIG=/path/to/config.json
  6. 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.

Environment variables

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)

Session access control

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.

Roadmap & decisions

License

MIT

About

Collaborative OpenCode session sharing. Pair-program a live AI session from another OpenCode instance in real time.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages