A shared message bus for humans and AI agents. The bus is a directory — carried by git, or a plain/shared filesystem.
Status (8.x). Crosstalk is a single npm package —
@cordfuse/crosstalk(the separate@cordfuse/crosstalkddaemon package is deprecated; the daemon lives ascrosstalk daemon dispatchinside this same package). CLI surface is strict noun-verb, mirroring@cordfuse/llmux. As of 8.x the bus is transport-pluggable: git (distributed, versioned, auditable) is the default; a local or shared filesystem —transport: local, including a mounted SMB/NFS share — needs no git and no remote. Select per transport with thetransport:key indata/crosstalk.yaml.
- Transport — the message bus, scaffolded by
crosstalk transport init. A directory: a git repo (default — distributed, versioned) or a plain/shared filesystem (transport: local). It IS the bus.- Machine — a host running one crosstalk engine. Identifies itself via the dispatcher
--alias(defaulting to the transport name).- Message — a markdown file with YAML frontmatter, written to a channel. The unit of work.
- Model — a named agent invocation declared in
data/crosstalk.yaml(e.g.sonnet,codex-o3).- Actor — an optional persona file (
local/actors/<name>.md) that prepends to a model's prompt as system context.- Channel — a UUID directory under
data/channels/. A conversation thread. Optionally parented.If you remember those six, the rest of this README will read smoothly.
AI coding agents are powerful individually. The moment you want two of them to coordinate, you hit the orchestration question — and every existing answer forces a trade.
- SDK-based frameworks (LangChain, CrewAI, AutoGen, "agent OS" tools) call vendor APIs directly. They're built around a specific runtime and compose with vendor SDKs, not the agent CLIs you already run. Vendor-coupled by design.
- Single-vendor CLI wrappers (Anthropic's Claude Agent SDK, GitHub Actions integrations like
claude-code-actionor OpenAI's Codex action) spawn one vendor's CLI very well. They don't mix vendors, and they don't model agent-to-agent peer messaging. - Workflow orchestrators (Temporal, Airflow, n8n) run anything you give them, but require their own server and scheduler. Not built for peer messaging between long-lived agents.
- Custom shell glue wiring
claude --printintocodex execworks for a one-shot demo. Breaks at the second machine. No audit trail, no retry semantics, no durability.
The missing piece: a way to make any agent CLI talk to any other agent CLI, across machines, with no broker, where the conversation is durable and auditable for free.
Crosstalk is that missing layer. The whole protocol is one idea — a directory is the message bus. Carry that directory with git (distributed, versioned, auditable) or a plain/shared filesystem (local, LAN, a mounted share) — same protocol either way.
- Any CLI participates. If an agent's CLI runs one prompt non-interactively and prints a reply, it's a valid model. Mix Claude Code, Codex, Gemini CLI, Qwen Code, opencode, Antigravity — or any future CLI that follows the same shape — in one transport.
- Messages are files. Every send is a markdown file with YAML frontmatter dropped into a channel. On the git transport each is a commit, so the whole conversation is git history — nothing to lose, nothing hidden, nothing to back up separately. On the filesystem transport it's just the files on disk.
- Peer-to-peer. Each machine runs its own crosstalk engine. No broker, no central runtime, no registry.
- Multi-machine for free. Git already solves "synchronize this across hosts"; a shared mount (SMB/NFS) does the same on a LAN. Crosstalk inherits whichever you point it at.
- Self-coordinating. Collision-free filenames mean concurrent writers never clash; the git transport adds rebase-retry on push, the filesystem transport relies on the unique filenames alone. Either way the bus works correctly with no central coordinator.
No infrastructure to provision, no central server holding your conversation — just a directory, on git or a filesystem.
A Crosstalk model is any CLI that:
- accepts a prompt (the dispatcher appends it as the CLI's last argument), and
- prints its reply to stdout, then exits
0.
That's it. No SDK, no plugin, no adapter. See GUIDE-CLI.md for ready-made entries per agent CLI.
User mode is the right choice for solo dev or any setup where the operator IS the only participant on this machine. The daemon runs as your own user; auth tokens land in the regular ~/.claude, ~/.codex etc. spots; no sudo needed.
# 1. Install the CLI. One package now — daemon is a subcommand.
npm install -g @cordfuse/crosstalk
# (Requires Node 20+ and git on PATH.)
# 2. Opt into user mode for this shell session (system mode is the default).
export CROSSTALK_USER_MODE=1
# 3. Scaffold a transport. Default name is 'crosstalk'; pass
# --containername <name> for a second/named one. Storage lives under
# ~/.local/share/crosstalk/<name>/ (or the per-OS XDG equivalent).
crosstalk transport init
# 4. First-run setup. The engine prints a /setup?token=... URL at boot —
# visit it once to create the admin user. After that, CLI auth lives at
# ~/.config/crosstalk/credentials.json (multi-profile; see `auth list`).
crosstalk server start
# (Open the printed URL, finish the wizard, then return here.)
crosstalk auth login
# 5. Edit data/crosstalk.yaml — uncomment a provider block (e.g.
# anthropic-personal) and drop your env vars into auth/<provider>.env
# per transport/auth/README.md. Install the agent CLI in your shell:
npm install -g @anthropic-ai/claude-code
# 6. Create a channel.
crosstalk channel create general
# 7. Send a primitive. The engine picks it up, invokes Claude,
# and commits the reply back into this same git repo.
crosstalk message send --to sonnet "What is the capital of France?"
# 8. Wait a few seconds, then check for replies:
crosstalk message replies <relPath printed by step 7>
# Lifecycle: stop / status / tail logs / restart
crosstalk server stop
crosstalk server status --probe
crosstalk server logs -f
crosstalk server restartThat's the entire surface area, single-machine. The engine reads data/crosstalk.yaml, checks PATH for the first token of each entry, and claims the models whose CLI is installed.
To set up a remote, either pass --remote <url> to crosstalk transport init at scaffold time, or cd into ~/.local/share/crosstalk/<name>/transport/ and run raw git.
System mode is the right choice for a headless host, a shared workstation, or any deploy where you want the engine to start on boot, run as a non-login service user, and be controlled by systemd. This is the default mode (no env var to set).
# 1. Install the CLI globally.
sudo npm install -g @cordfuse/crosstalk
# 2. Run the installer — creates the 'crosstalk' system user, /etc and
# /var/lib directories, and the systemd template unit.
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/cordfuse/crosstalk/main/deploy/install.sh)"
# Or, if you cloned the repo: sudo ./deploy/install.sh
# 3. Initialize a transport AS the service user.
sudo -u crosstalk crosstalk transport init --containername main
# 4. Enable + start the systemd instance for it.
sudo systemctl enable --now crosstalk@main
sudo systemctl status crosstalk@main
sudo journalctl -u crosstalk@main -f # or: tail -f /var/log/crosstalk/main.log
# Additional named transports: repeat with a different --containername.
sudo -u crosstalk crosstalk transport init --containername staging
sudo systemctl enable --now crosstalk@stagingStorage lives at /var/lib/crosstalk/<name>/, owned by the crosstalk user. Agent CLI auth tokens land under /var/lib/crosstalk/.claude/, /var/lib/crosstalk/.codex/, etc. — the service user's home. The daemon never touches /home/*.
In system mode, crosstalk server start|stop|restart invocations from the CLI print a hint redirecting the operator to sudo systemctl <verb> crosstalk@<name> — the right lifecycle when systemd owns the process.
Models are declared in data/crosstalk.yaml — nested under providers. Each provider may declare an env_file: pointing at a per-provider dotenv file under auth/. The engine reads it at agent spawn and merges into the subprocess env. Addressing is <provider>/<model> (bare names also work when unambiguous).
# data/crosstalk.yaml
providers:
anthropic-personal:
env_file: auth/anthropic-personal.env # CLAUDE_CODE_OAUTH_TOKEN=...
models:
sonnet: claude --print --dangerously-skip-permissions --model sonnet
haiku: claude --print --dangerously-skip-permissions --model haiku
google-personal:
env_file: auth/google-personal.env # GEMINI_API_KEY=...
models:
gemini-pro: gemini --skip-trust --yolo -p --model gemini-1.5-pro
agy: agy --print
openrouter:
env_file: auth/openrouter.env # OPENAI_API_KEY=sk-or-... + OPENAI_BASE_URL=...
models:
qwen3-coder: qwen --auth-type openai --yolo --model qwen/qwen3-coderPer-agent install commands + the env vars each agent reads are documented in transport/auth/README.md. For the auth schema design rationale, see V8-AUTH-DESIGN.md.
For multi-step work you have two paths:
# Interactive — describe what you want in plain language; engine compiles
# it into a YAML plan, you review/edit, then submit:
crosstalk workflow compose "Fan out 3 sonnet drafts and have opus synthesize"
# Or hand-author the YAML/markdown and submit directly:
crosstalk workflow run workflows/review-and-synthesize.mdExample workflows/review-and-synthesize.md:
---
type: workflow
to: opus@desktop
as: orchestrator
---
1. Fan out 3 junior developers running sonnet@laptop. Each drafts the
proposal in their own voice.
2. Send the 3 drafts to a reviewer running opus@desktop to synthesize
into a single coherent proposal.
3. Return the synthesis to the original requester.The runtime auto-creates a child channel and takes over deterministically: one LLM call compiles the prose body into a {fanout, synthesize} JSON plan, plan.fanout.count sub-primitives fire into the child channel (scoped via the dispatcher registry to avoid duplicate work across hosts), then once their replies are all in, one synthesis sub-primitive aggregates them, and the synthesis reply gets routed back to you in the parent channel.
crosstalk workflow status lists in-flight workflows + their phase (compile / fanout / synthesize / complete / failed).
- GUIDE-CLI.md — drive Crosstalk with the
crosstalkcommand. For scripts and operators who like a terminal. - GUIDE-PROMPTS.md — drive Crosstalk in plain language.
crosstalk chat <agent>spawns an interactive agent CLI on the host that readsPROTOCOL.mdfrom the transport and runscrosstalkcommands on your behalf. The agent process is a foreground child — when you exit, it's gone (no persistent live agents; crosstalk is async-by-design).
The local-only quickstart left your transport as a plain local git repo — no origin, no push. That's fine for solo experimentation. To collaborate or run a second machine, push the transport to any git host that takes ssh:// or https://.
Example with GitHub:
-
Create an empty repo on GitHub. Don't check the "initialize with README" box — the transport already has the right contents, and any pre-seeded files will conflict on first push. A private repo is the common choice (a transport is your conversation history; treat it like a project repo, not a public one).
-
From inside your local transport directory:
git remote add origin git@github.com:<you>/<your-transport>.git git push -u origin main
Use SSH (
git@github.com:…) rather than HTTPS — the dispatcher commits replies back continuously, and SSH avoids the credential prompts an HTTPS remote would otherwise require. -
The push will include
CROSSTALK-VERSION,PROTOCOL.md,CROSSTALK.md,data/crosstalk.yaml,local/actors/orchestrator.md, and any channels you've created. No state, no secrets — everything in the repo is meant to be shared with the other participants.
Same flow works for self-hosted Gitea or GitLab. Crosstalk doesn't care; it only talks git.
Heads up: SSH access. Every machine that runs the engine needs to be able to
git pushto the transport repo, because the engine commits replies and pushes them. In user mode the daemon uses your operator~/.ssh/; in system mode it uses/var/lib/crosstalk/.ssh/(drop a deploy key there as thecrosstalkuser).
Don't want a git remote? Point Crosstalk at a plain directory instead — a local folder for a single host, or a mounted SMB/NFS share for several hosts on a LAN. No git, no remote, no infrastructure.
# Scaffold a local-fs transport straight onto the directory (e.g. a NAS mount):
crosstalk transport init --transport local --path /mnt/share/crosstalk
# Run the engine on it — the bus lives on the share, state stays machine-local:
crosstalk server start --path /mnt/share/crosstalk--transport local writes transport: local into data/crosstalk.yaml (no git init), and the dispatcher self-selects the filesystem transport: it reads/writes message files directly and picks up new ones via an mtime cursor — event-driven fs.watch on a true local FS, polling over a network mount. Each host that mounts the share runs its own engine and keeps its own machine-local cursor/heartbeat, so concurrent writers never clash (collision-free filenames).
Tradeoffs vs. git: no version history, no audit trail, no conflict resolution beyond the unique filenames — but zero setup and no SSH/credentials. Reach for git when what was said matters; reach for the filesystem transport for quick local/LAN swarms.
SFTP? Mount it with
sshfsand usetransport: local— Crosstalk just sees a directory.
Multi-machine adds exactly one new idea: routing by alias.
# On the second machine — user mode example
export CROSSTALK_USER_MODE=1
crosstalk transport init --remote <your-transport-url>
crosstalk server startNow any send addressed to --to sonnet@server lands on the server (alias defaults to the transport name; override at start time). Bare --to sonnet (no @machine) reaches whichever dispatcher claiming sonnet picks it up first.
There are no host files to author. There is no per-machine declaration committed to the transport. A machine's identity is just its dispatcher alias. The model registry (data/crosstalk.yaml) is shared by everyone; each dispatcher claims the entries whose CLI is installed on that machine.
Full protocol: transport/CROSSTALK.md.
The CLI can target any reachable engine — local loopback, another machine on your LAN, a teammate's box over Tailscale. Multi-profile auth lets one CLI hold credentials for several engines and switch between them per command.
The engine binds 127.0.0.1 only out of the box. The bearer-token gate is enforced on every operational endpoint regardless of bind address — even local CLI calls authenticate the same way a remote one would. Loopback is defense-in-depth, not the security boundary.
To accept connections from off-host, set CROSSTALK_API_BIND=0.0.0.0 (or a specific interface) at engine start. Don't expose bare HTTP on a public IP — auth alone doesn't replace TLS. Front the daemon with something that terminates TLS and validates source (Tailscale serve recommended; nginx / Caddy also fine).
# On the host running the engine — leave the engine bound to loopback,
# let Tailscale terminate TLS on the tailnet edge.
tailscale serve --bg --https=3445 http://localhost:7000
tailscale serve --bg --http=3082 http://localhost:7000(Substitute the engine's actual port — 7000 for the default transport, 7001+ for named ones. Find it at <base>/<name>/api-port or in crosstalk server status.)
The host is now reachable as https://<host>.tailnet.ts.net:3445 from any tailnet device.
Cordfuse port conventions (each app fronted on its own pair so multiple tools coexist on one tailnet host):
| App | HTTP front | HTTPS front |
|---|---|---|
| llmux | 3080 |
3443 |
| vyzr | 3081 |
3444 |
| crosstalk | 3082 |
3445 |
# Add the remote engine as a new profile alongside the local default.
crosstalk auth login --server https://crosstalk.example.com:3445 --profile prod
# Prompts for username + passphrase. Bearer token stored at
# ~/.config/crosstalk/credentials.json (mode 0600). The local 'default'
# profile is untouched.
crosstalk auth list
# PROFILE USERNAME SERVER
# * default steve http://127.0.0.1:7000
# prod ops https://crosstalk.example.com:3445Three ways, in priority order:
- Per command:
crosstalk --profile prod message send --to sonnet "..."orcrosstalk --server <url> ... - Switch the active profile:
crosstalk auth use prod— every subsequent command targetsproduntil you switch back - Default profile: the active profile from
credentials.json
A few things still need to be on the engine's host:
crosstalk server start|stop|restart— these operate the local engine process (the systemd / pidfile lifecycle). They have no remote mode.crosstalk chat <agent>— spawns a foreground agent CLI in your terminal. The engine doesn't proxy it.- The transport's git remote (separate from the engine's HTTP API) — each engine still needs
git pushaccess to the bare origin to commit replies. The "Putting the transport on a git remote" + "Adding a second machine" sections above cover that.
Everything in the CLI guide under channel, message, workflow, agent, logs, settings, status, token works against any profile.
crosstalk daemon dispatch is what runs under the hood — crosstalk server start spawns it as a detached host process (user mode), and the systemd unit invokes it as ExecStart= (system mode). Operators don't typically run it directly; the wrappers exist so the lifecycle is the same in both modes.
| Subcommand | Purpose |
|---|---|
crosstalk daemon dispatch --alias <name> |
The dispatch loop. Started by crosstalk server start or the systemd unit. |
crosstalk daemon init <dir> |
Scaffold a transport template into <dir>. Used internally by crosstalk transport init. |
crosstalk daemon stop |
Signal SIGTERM to the running dispatcher via pidfile. |
Every operational HTTP endpoint requires bearer-token auth:
/healthz,/version— open (monitoring probes).- Everything else (
/channels,/messages,/status,/replies,/agents/installed, all/api/*) — requiresAuthorization: Bearer <token>(or the equivalent session cookie for the web UI).
First-run setup is a one-time web wizard. The engine boot prints a /setup?token=… URL (logged as auth_first_run_setup_token); the operator visits it to create the first admin user. After that:
- The web UI uses cookie auth set on login.
- The CLI stores credentials at
~/.config/crosstalk/credentials.json(XDG-aware, mode 0600), obtained viacrosstalk auth login. The file is multi-profile (one CLI, multiple engines); seecrosstalk auth list. Each call sendsAuthorization: Bearer …from the active profile automatically.
401 responses include a hint pointing at crosstalk auth login so operators discover the auth flow without reading docs.
User/token records live under <base>/<name>/crosstalk-state/auth/ (users.json, tokens.json, mode 0600; never pushed to git).
The engine serves a complete web UI on the same loopback port. After crosstalk server start, visit http://127.0.0.1:<port>/ in a browser. Routes:
| Route | Purpose |
|---|---|
/ |
Dashboard — engine heartbeat, claimed models, channels, pending work, SSE live updates |
/c, /c/<handle> |
Channel list + threaded message view; send / rename / delete inline |
/w |
Workflow list + compose form (natural-language → POST /api/workflows/compose → editable YAML preview → POST /api/workflows/submit) |
/w/<childUuid> |
Live workflow detail with phase badge (compile / fanout / synthesize / complete / failed), per-dispatch + per-reply rows, completion banner, opt-in browser notification |
/agents |
Installed CLIs + yaml-referenced + currently-claimed catalog |
/tokens |
Mint + revoke API tokens; plaintext shown once at mint |
/account |
Sign out + change passphrase |
/users |
Admin only — create / delete / toggle-admin (self-protected) |
/logs |
Live SSE tail of the engine's structured event stream (/api/logs/stream) with filter + pause |
/settings, /about |
Read-only engine snapshot + product info |
Off-host access (e.g., from a phone): the engine binds 127.0.0.1 only — front it with tailscale serve or similar TLS-terminating proxy.
tsx— run TypeScript directly without a build step (engine source lives undersrc/).yaml— frontmatter anddata/crosstalk.yamlparsing.
alpha.18 collapsed the v7 client/ + engine/ split into one package:
bin/crosstalk.js— the noun-verb CLI dispatcher (operator-facing entry).commands/— per-noun handlers (auth,token,server,transport,channel,message,workflow,chat,agent,logs,settings,daemon,version).lib/— shared client-side helpers (api client, credentials store, argv parser, error reporter, native engine lifecycle).src/— TypeScript engine code, executed viatsxat run time.template/— seed transport template thatcrosstalk transport initcopies into a new transport (protocol spec, model registry, agent orientation prompts).deploy/—install.sh+crosstalk@.servicesystemd template for system-mode deployments.transport/— protocol artifacts (CROSSTALK.md, PROTOCOL.md, …) shared between client + engine.
Operator guides at the repo root:
- GUIDE-CLI.md — full noun-verb reference for every
crosstalksubcommand. - GUIDE-PROMPTS.md — drive Crosstalk in plain language by
cd-ing into a transport and running an agent CLI directly.
Design docs: V7-SPEC.md (the v7 protocol spec, still authoritative), V8-AUTH-DESIGN.md (v8 auth schema), V8-CLI-ALIGN.md (the alpha.18 noun-verb plan + migration table).
alpha.18 is a clean break — every old verb form is gone, no shims. The full mapping lives in V8-CLI-ALIGN.md; the highlights:
| Old | New |
|---|---|
crosstalk init |
crosstalk transport init |
crosstalk rm |
crosstalk transport rm |
crosstalk channel <name> (bare) |
crosstalk channel create <name> |
crosstalk channel <h> --rename <new> |
crosstalk channel rename <h> <new> |
crosstalk channel <h> --delete |
crosstalk channel delete <h> |
crosstalk run --type primitive --to <m> <body> |
crosstalk message send --to <m> <body> |
crosstalk run --type workflow <file> |
crosstalk workflow run <file> |
crosstalk replies <relPath>... |
crosstalk message replies <relPath>... |
crosstalk token mint <name> |
crosstalk token create <name> |
crosstalk chat --agent <name> |
crosstalk chat <name> (positional) |
crosstalk up / down / restart / pull / logs (v7 shims) |
gone — use crosstalk server start / stop / restart and crosstalk server logs |
crosstalkd bin |
gone — use crosstalk daemon |
@cordfuse/crosstalkd package |
deprecated — @cordfuse/crosstalk is now the only package |
~/.config/crosstalk/cli-token |
auto-migrated to ~/.config/crosstalk/credentials.json on first read; old file deleted |
systemd unit crosstalkd@<name> |
crosstalk@<name> (rerun deploy/install.sh) |
Transport git repos are portable — the storage path is unchanged (~/.local/share/crosstalk/<name>/transport/ in user mode; /var/lib/crosstalk/<name>/transport/ in system mode). You don't need to re-init.
If you have a v7 Docker install:
docker compose -f <your-compose-file> downfirst.npm install -g @cordfuse/crosstalk@latest— pulls the latest 8.x.- If you previously installed
@cordfuse/crosstalkd:npm uninstall -g @cordfuse/crosstalkd(deprecated, no longer published since alpha.18). - In user mode:
export CROSSTALK_USER_MODE=1 && crosstalk server start --containername <name>. - In system mode: rerun
sudo ./deploy/install.shto lay down the newcrosstalk@.serviceunit, thensudo systemctl daemon-reload && sudo systemctl enable --now crosstalk@<name>. The oldcrosstalkd@<name>.servicefiles can be removed.
The v7 GHCR image ghcr.io/cordfuse/crosstalk-server:* is no longer being updated.
Crosstalk 8.x — single-package monorepo + strict noun-verb CLI, transport-pluggable (git or a local/shared filesystem). The protocol version lives at CROSSTALK-VERSION (single integer) at the transport root. Published as @cordfuse/crosstalk.