Skip to content

cordfuse/crosstalk

Repository files navigation

Crosstalk

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/crosstalkd daemon package is deprecated; the daemon lives as crosstalk daemon dispatch inside 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 the transport: key in data/crosstalk.yaml.


Six words that come up everywhere on this page

  • 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.


The problem

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-action or 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 --print into codex exec works 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.

The solution

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.


The one contract

A Crosstalk model is any CLI that:

  1. accepts a prompt (the dispatcher appends it as the CLI's last argument), and
  2. 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.


Install + try it locally (user mode)

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 restart

That'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.


Install + run as a service (system mode)

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@staging

Storage 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.


Pick your agent

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

Per-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.


Workflows

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.md

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


Two ways to use it

  • GUIDE-CLI.md — drive Crosstalk with the crosstalk command. 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 reads PROTOCOL.md from the transport and runs crosstalk commands 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).

Putting the transport on a git remote

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:

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

  2. 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.

  3. 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 push to 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 the crosstalk user).


Local filesystem transport (no git)

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 sshfs and use transport: local — Crosstalk just sees a directory.


Adding a second machine

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 start

Now 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.


Remote operation

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.

Defaults

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

Tailscale-fronted (recommended)

# 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

Logging into a remote engine

# 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:3445

Switching engines

Three ways, in priority order:

  • Per command: crosstalk --profile prod message send --to sonnet "..." or crosstalk --server <url> ...
  • Switch the active profile: crosstalk auth use prod — every subsequent command targets prod until you switch back
  • Default profile: the active profile from credentials.json

What stays local

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 push access 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.


Daemon internals

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.

Engine subcommands

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.

Auth model

Every operational HTTP endpoint requires bearer-token auth:

  • /healthz, /version — open (monitoring probes).
  • Everything else (/channels, /messages, /status, /replies, /agents/installed, all /api/*) — requires Authorization: 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 via crosstalk auth login. The file is multi-profile (one CLI, multiple engines); see crosstalk auth list. Each call sends Authorization: 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).

Web UI

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.

Dependencies

  • tsx — run TypeScript directly without a build step (engine source lives under src/).
  • yaml — frontmatter and data/crosstalk.yaml parsing.

Repository layout

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 via tsx at run time.
  • template/ — seed transport template that crosstalk transport init copies into a new transport (protocol spec, model registry, agent orientation prompts).
  • deploy/install.sh + crosstalk@.service systemd 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 crosstalk subcommand.
  • 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).


Upgrading from alpha.17 / v7

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:

  1. docker compose -f <your-compose-file> down first.
  2. npm install -g @cordfuse/crosstalk@latest — pulls the latest 8.x.
  3. If you previously installed @cordfuse/crosstalkd: npm uninstall -g @cordfuse/crosstalkd (deprecated, no longer published since alpha.18).
  4. In user mode: export CROSSTALK_USER_MODE=1 && crosstalk server start --containername <name>.
  5. In system mode: rerun sudo ./deploy/install.sh to lay down the new crosstalk@.service unit, then sudo systemctl daemon-reload && sudo systemctl enable --now crosstalk@<name>. The old crosstalkd@<name>.service files can be removed.

The v7 GHCR image ghcr.io/cordfuse/crosstalk-server:* is no longer being updated.


Status

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.

About

Agent-agnostic swarm communication protocol over a shared directory — git or filesystem transport. Humans and AI agents across machines. Peer-to-peer, no broker.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors