ouija
A transparent pipe between coding sessions through tmux.
You're deep in a coding session when you realize another session has the understanding you need. Maybe it started on something unrelated on your machine, maybe it's on your laptop at home. You say "ask deploy-infra what port the gateway uses." The other session receives it as natural input, draws on what it knows, and replies. Every session stays fully interactive for you.

Ad hoc by design. Sessions don't need to be started any special way. Run ouija, open coding sessions as you normally would, and they discover each other. For same-machine messaging that's all you need. For cross-machine, pair two ouija daemons once over Nostr and any session on either machine becomes reachable.
ouija is plumbing, not a harness. It delivers messages between sessions and manages their lifecycle. The protocol is open, the transport is end-to-end encrypted, and sessions keep their own memory, tools, and context. Today ouija supports Claude Code (primary, well-tested), opencode (integrated, less battle-tested), and the Codex CLI (TUI-injection backend — see docs/codex-cli.md). Additional backends would plug in through the same hook and API surface the existing ones use.
The session lifecycle primitives (spawn, kill, health checks, worktree isolation) may also be useful beneath a higher-level harness or orchestrator, but ouija itself stays small: discover sessions, deliver messages, and keep sessions reachable.
Prerequisites
tmux and at least one supported coding assistant: Claude Code, opencode, or the Codex CLI.
Quick start
If a matching prebuilt release artifact is available for your platform, cargo binstall ouija can install it without compiling.
ouija start-server runs the daemon in the foreground. Keep it running in that terminal, or run it under your service manager.
The daemon auto-configures your coding assistant (hooks, skills). In another terminal, open a session inside tmux:
&&
Sessions auto-register using the working directory name (e.g. /code/api becomes api). Start talking:
"Use ouija to ask deploy what port the gateway is exposed on"
What you can do
Message any session, local or remote. Sessions discover each other automatically. Messages travel through a short CLI command (ouija ask target "question") instead of making the assistant compose raw HTTP requests.
Share state through the filesystem, not just the wire. A message can be small and point to shared state: "see docs/api.md" or "check the worktree at ~/code/foo". The receiver loads the full content from disk, bypassing the compression that any fixed-size message would impose. Messages as pointers to shared state scale better than messages as state.
Spawn sessions on the fly. Ask the assistant to start a new session (e.g. "use ouija to start a gateway-debug session"). The daemon creates a tmux window, launches a coding session, and registers it. You can specify a prompt to seed the session with context and a backend (claude-code, opencode, or codex-cli).
Long-running work. Two mechanisms for recurring work:
- Loops -- the session drives itself. Simple — the session's prompt and reminder tell it what to do and how to signal completion. The daemon handles the restart cycle.
- Tasks (cron) -- the daemon drives the session. Good for periodic checks, daily reports, scheduled maintenance. If the target session is dead, the daemon revives it with the task's prompt + reminder.
Peer-to-peer collaboration. No hierarchy. Two long-running sessions can message each other directly — one optimizing a skill while the other evaluates results, or one migrating files while the other reviews the diffs. They coordinate through the ouija skill's send capability, not through a central orchestrator.
Always interactive. Every session runs in a tmux pane. You can jump into any session at any time — watch it work, type a correction, answer a question, or take over. The session doesn't know or care whether the next input comes from a peer session or from you at the keyboard.
Worktree sessions. Spawn sessions in isolated git worktrees for parallel work on the same repo without branch conflicts.
Nostr DMs. If you use Nostr, configure your npub to control the daemon from any Nostr client. Send /list, /start, @session message, or bare text (routed by an LLM).
Dashboard at http://localhost:7880. Manage sessions, tasks, node connections, and settings.
Design philosophy
ouija is transport, not intelligence. Sessions compose their own messages, interpret what they receive, and decide what to do. ouija delivers bytes. That is deliberate.
Messages are compression. When a session sends a message, it is compressing its current understanding into a few hundred tokens. The transport is lossless but the composition is lossy. For anything larger than a paragraph, prefer pointing at shared state (a file, a wiki page, a worktree) rather than dumping context into the message body.
Receiving sessions can drop information. Even when a message arrives intact, the receiver may fail to integrate it with its existing context. This is a property of LLMs, not ouija. Treat inter-session messaging as persuasion, not injection: explicit, cited, and verifiable against shared artifacts.
Stale claims transfer invisibly. If session A tells session B "the database is sharded by tenant," and A's mental model is actually outdated, B will treat the claim as fact. Prefer pointers to ground truth over assertions whenever it matters.
Connecting machines
On machine A:
On machine B:
Sessions on both machines discover each other. Tickets contain a connect secret, only authorized nodes can communicate. After connecting, both nodes remember each other and auto-reconnect on restart.
Message protocol
Sessions communicate through XML messages delivered to the coding assistant:
what port does the gateway use?
Messages can reference earlier ones for conversation threading:
re="47"— progress update on task 47re="47" done="true"— task 47 is complete
The daemon assigns unique IDs to every message, tracks pending replies, and nudges sessions that haven't responded. Sessions interact via the ouija CLI and the ouija skill -- the XML is handled automatically.
How it works
- Each machine runs an ouija daemon (small Rust binary)
- Sessions auto-register via hooks on startup
- Local messages: tmux injection (Claude Code) or HTTP API (opencode)
- Remote messages: end-to-end encrypted over Nostr relays. No central server, no direct TCP connection required, works across NATs, and relays see only ciphertext. Unusual for agent communication, since most frameworks assume a reachable IP or a proprietary cloud.
- Node auth: connect secret in the ticket, unknown senders rejected
All session state transitions go through a pure state machine (DaemonProtocol) with a Stateright model check in daemon_protocol.rs.
Security
- Tickets are secrets. Share out-of-band only (copy/paste, not through the assistant).
- Connect secret auth. Unknown senders are rejected.
- Encrypted transport. End-to-end encrypted via Nostr (NIP-17 gift-wrapped DMs). Relays cannot read content.
- Localhost only. The daemon binds to
127.0.0.1. - Keep tickets out of chats. Normal session APIs do not expose tickets, but any assistant with shell access can run CLI commands you ask it to run.
- Persistent logs are metadata-only.
messages.jsonlrecords routing metadata; the dashboard may show recent in-memory message content until restart.
Claude Code permissions
Ouija does not force Claude Code into bypass-permissions mode by default. Spawned Claude sessions inherit your Claude Code settings, such as permissions.defaultMode in ~/.claude/settings.json.
If you run ouija inside an isolated environment and want spawned Claude sessions to skip permission prompts, opt in explicitly:
To return to Claude Code's own default behavior:
Codex model routes
Ouija-launched Codex sessions normally use Codex's own default home resolution
($CODEX_HOME or ~/.codex). You can add a Codex-specific route so a
user-facing model alias selects a provider-specific Codex home without exposing
that detail on every session:
The alternate Codex home owns its own config.toml and provider setup. For
Gemini, run a local Responses-compatible sidecar such as LiteLLM on localhost
and point that Codex home at the proxy; the Gemini API key stays in the sidecar
environment. Sessions without --model gemini continue using the normal Codex
default, for example gpt-5.5 with whatever reasoning effort is configured in
~/.codex/config.toml. Passing Ouija --effort low to a Codex session overrides
that for the launched session with -c 'model_reasoning_effort="low"'.
CLI
For generated or multi-line message bodies, prefer --stdin or
--message-file on ask, tell, and reply. That avoids shell expansion of
backticks, $(), quotes, and JSON before ouija receives the message.
spawn-session requires explicit lifecycle ownership: choose either
--parent-session <SESSION_ID> or --no-parent-session, and choose
--idle-policy keep-open, --idle-policy ask-parent-when-done, or
--idle-policy close-when-done.
Outside tmux, such as an OpenCode HTTP/API tool process, run ouija whoami to resolve your own session id and pass its exact output as the sender: ouija ask <to> "msg" --from <public-ouija-id>. Never guess a sender id (project directory name, branch name, or an ouija ls entry) — the daemon rejects sender claims it can disprove. Do not use backend labels like opencode or opaque OpenCode backend_session_id values as --from.
Run ouija --help for the full command list.
Data
Config in ~/.config/ouija/ (settings, identity). Data in ~/.local/share/ouija/ (sessions, tasks, connections). Persistent message logs are metadata-only.
Tmux integration
Windows are automatically named after the ouija session when the pane is the only one in the window. Each pane also gets a @ouija_session user variable you can use in your tmux config for more control:
set -g window-status-current-format '#{?@ouija_session,⊕ #{@ouija_session},#{b:pane_current_path}}'
Fuzzy session pickers that read tmux's display format will show ouija session names automatically. The author uses dcadenas/tmux-sessionizer, a fork that expands all sessions into window-level entries (e.g. ouija/1:⊕ daily-report), making ouija sessions easy to find and switch to.
Testing
# Unit tests (the expensive Stateright model check is ignored by default)
# Full Stateright model check (CPU-intensive; run explicitly)
# Local + nostr + opencode e2e, in Docker
# Only local e2e
# Only nostr P2P e2e (relay + 4 daemons + auth tests)
# Only opencode integration e2e
# Install/preflight tests (clean machine, no Rust)