Iroh-backed bridge that multiplexes a few local coding agents — Codex, Pi, OpenCode, Claude, and Factory Droid — onto a single QUIC connection. Run the daemon on your machine, scan a QR with a paired client, and the client picks an agent over the same stream multiplexer.
End-user packages are published from the kittylitter project, which wraps this daemon under the kittylitter command. The currently enabled published channel is npm/bun; Homebrew, shell installer, PowerShell installer, and MSI publishing are not enabled in the release config right now. Source installs from this repo expose the daemon as alleycat.
| Platform | Install |
|---|---|
| npm / bun | npm install -g kittylitter or bunx kittylitter |
| From source | cargo install --path crates/alleycat (this repo) |
kittylitter install # autostart at login (no admin)
kittylitter status # node id, token, agent availability
kittylitter pair --qr # phone-side QRUse alleycat instead of kittylitter when running a source build from this repo.
The install command registers a launchd user agent on macOS, a systemd --user unit on Linux (with .desktop autostart fallback), or a Startup-folder shortcut on Windows. None of them require sudo.
The daemon spawns external coding-agent CLIs on demand — install whichever ones you'll use:
| Agent | Install |
|---|---|
claude |
npm install -g @anthropic-ai/claude-code (or bun install -g @anthropic-ai/claude-code). Then claude /login once. |
opencode |
See opencode docs. |
pi |
See pi-mono docs. |
codex |
Install the codex CLI (codex docs). The daemon spawns codex app-server on demand. |
droid |
Install Factory Droid, then either run droid login once or set FACTORY_API_KEY in the daemon environment. |
The command surface is the same for kittylitter packaged installs and alleycat source builds. The table uses alleycat because this is the daemon repo.
| Command | What it does |
|---|---|
alleycat serve |
Run the daemon in the foreground (what install autostarts). |
alleycat install / uninstall |
Per-user autostart, idempotent. |
alleycat status [--json] |
Pid, node id, token fingerprint, uptime, agent availability. Falls back to a file-only readout if the daemon isn't running. |
alleycat pair [--qr] |
Print the stable pair payload, optionally with an ASCII QR code. |
alleycat rotate |
Mint a fresh token. Node id is preserved; the running daemon picks up the new token immediately. |
alleycat reload |
Re-read host.toml and swap agent config without restarting. |
alleycat agents list |
List configured agents and their availability. |
alleycat logs [-f] |
Tail the daemon log files. |
alleycat stop |
Graceful shutdown via the control socket. |
The daemon talks to the CLI over a Unix domain socket on macOS/Linux and a per-user named pipe on Windows. status, pair, and rotate round-trip through it when the daemon is up and fall back to file-only operations when it isn't, so first-run flows still work.
| Agent | Spawned by daemon? | How |
|---|---|---|
codex |
Yes, one shared backend | Lazy spawn of codex app-server --listen ws://<host>:<port> on first connect. The child is kept alive for the daemon lifetime; each iroh stream becomes a fresh websocket client and conversations persist independently of any single client. |
pi |
Yes, per codex thread | PiPool spawns pi --mode rpc on demand, bounded at 16 processes with a 10-minute idle reap and LRU eviction. |
opencode |
Yes, one shared backend | Lazy spawn of opencode serve --port=auto --auth-token=auto on first connect, gated on /global/health. Or set OPENCODE_BRIDGE_BACKEND_URL to point at an existing instance. |
claude |
Yes, per codex thread | ClaudePool spawns claude -p --input-format stream-json --output-format stream-json --session-id <thread_id> --dangerously-skip-permissions on demand. Same 16-cap, 10-minute idle reap, LRU eviction as pi. Sessions resume on next access via --resume <thread_id>. |
droid |
Yes, per codex thread | Spawns droid exec --input-format stream-jsonrpc --output-format stream-jsonrpc --cwd <cwd> and translates Factory session notifications into the codex app-server wire. |
alleycat pair prints:
{
"v": 1,
"node_id": "<iroh public key>",
"token": "<32-byte hex>",
"relay": null
}relay is optional and only set if the operator pinned a specific iroh relay in host.toml; otherwise iroh's default discovery applies. There is no port or cert fingerprint — iroh handles transport, the token authenticates the first JSON frame on every stream.
ALPN alleycat/1. Each iroh bidirectional stream begins with a length-prefixed JSON request:
{"op": "list_agents", "v": 1, "token": "..."}or
{"op": "connect", "v": 1, "token": "...", "agent": "codex"}The daemon answers with {ok, agents?, error?}. On connect, after the response the stream becomes the agent's native wire — websocket frames for codex (the daemon proxies straight to the shared codex app-server listener), JSON-RPC over JSONL for pi, opencode, claude, and droid.
host.toml is created on first run with sensible defaults; edit and alleycat reload to apply.
token = "..." # 32 bytes hex; rotate via `alleycat rotate`
# relay = "https://..." # optional iroh relay override
[agents.codex]
enabled = true
bin = "codex"
host = "127.0.0.1"
port = 8390
[agents.pi]
enabled = true
bin = "pi"
[agents.opencode]
enabled = true
bin = "opencode"
[agents.claude]
enabled = true
bin = "claude"
[agents.droid]
enabled = true
bin = "droid"
api_key_env = "FACTORY_API_KEY"Reload swaps config that's read per-request (token, agent enable flags). Codex's bin/host/port, pi's bin, OpenCode's bin/runtime port, and Droid's bin are pinned at first spawn; changing those requires alleycat stop + serve.
Per-OS, via directories::ProjectDirs:
| macOS | Linux | Windows | |
|---|---|---|---|
| Config | ~/Library/Application Support/dev.Alleycat.alleycat/host.toml |
$XDG_CONFIG_HOME/alleycat/host.toml |
%APPDATA%\Alleycat\alleycat\config\host.toml |
| State | (collapses to config dir) — host.key, host.lock, daemon.pid |
$XDG_STATE_HOME/alleycat/ |
%LOCALAPPDATA%\Alleycat\alleycat\data\ |
| Logs | ~/Library/Logs/dev.Alleycat.alleycat/daemon.log |
$XDG_STATE_HOME/alleycat/logs/ |
%LOCALAPPDATA%\Alleycat\alleycat\logs\ |
| Control IPC | $TMPDIR/alleycat-<userhash>/control.sock |
$XDG_RUNTIME_DIR/alleycat-<userhash>/control.sock |
\\.\pipe\alleycat-control-<userhash> |
| Autostart | ~/Library/LaunchAgents/dev.alleycat.alleycat.plist |
~/.config/systemd/user/alleycat.service (or ~/.config/autostart/alleycat.desktop) |
…\Startup\alleycat.lnk |
The Unix control socket falls through XDG_RUNTIME_DIR → state dir → TMPDIR → /tmp so the path always fits in sockaddr_un.sun_path (104 bytes on macOS/BSD, 108 on Linux), even under deeply nested hermetic test homes.
- Codex, Pi, OpenCode, Claude, and Droid children inherit
kill_on_dropsemantics, so they exit when the daemon does. alleycat stopshuts the iroh endpoint and the daemon process; launchd / systemd will restart it under their normal supervision.
cargo install --locked --path crates/alleycat
# or, for a workspace-relative build:
cargo build --release -p alleycat
target/release/alleycat installThe workspace crates are:
crates/alleycat—alleycatdaemon binary. Owns the iroh endpoint, the persistent identity, the agent dispatcher, and an OS-native control socket so the CLI can talk to the running daemon.crates/bridge-conformance— live conformance harness for comparing bridge behavior against codex app-server wire shapes.crates/bridge-core— shared JSON-RPC framing, server scaffolding, and notification plumbing used by the bridges.crates/codex-proto— shared codexapp-serverv2 wire shapes used by every bridge.crates/pi-bridge—pi-coding-agentprocess pool plus a codex-shaped JSON-RPC translator (one pi process per codex thread).crates/opencode-bridge— single sharedopencode servebackend wrapped in the same JSON-RPC surface.crates/claude-bridge—claude -p --output-format stream-jsonprocess pool wrapped in the same JSON-RPC surface (one claude process per codex thread).crates/droid-bridge— Factory Droidstream-jsonrpcprocess wrapper plus codex-shaped turn/tool translation (one droid process per codex thread).crates/claude-remote-control— auxiliary Claude remote-control protocol support.
Releases are produced from the litter repo, which carries this repo as a submodule under shared/third_party/alleycat and runs dist against the kittylitter wrapper to build platform release artifacts and publish the npm package. Homebrew, shell installer, PowerShell installer, and MSI publishing are disabled in litter's release config right now. To cut a release: bump version in the root Cargo.toml here, push, then bump the submodule pin in litter and tag vX.Y.Z there.
