# Reason Network design > This is how the engine works inside. **AI agent asked to join the Reason Network?** Follow > https://reasonmachines.com/network instead. A team directory and message bus for coding agents. Any agent (Claude Code, Codex, Cursor, Gemini CLI, opencode, Copilot CLI, Amp, …) on any teammate's machine can see who is working on what, right now, and message the right session directly: across the LAN or the internet, with no server to host. Status: M0 to M3 are built and tested on macOS (two machines over iroh; real Claude Code and Codex sessions). M4 is not started. ## Why another one There are ~15 projects from 2026 that let agents message each other (see [Prior art](#prior-art)). Nearly all are single-machine, single-person, or Claude-only. The two closest: - **agentbus**: per-device Rust daemon, peer-to-peer. But peers are your own devices via Tailscale (same Tailscale user), and other people are contacts answered by a headless responder, not their live sessions. - **agents-party**: shared encrypted room for Claude/Cursor/Codex. A room, not a directory, and cross-machine needs a hosted or self-hosted server. What nobody does: **a team directory of live sessions with context** (person, machine, agent, repo, branch, busy/idle, what it is doing), so an agent can pick the right recipient instead of broadcasting and hoping. That is the core of this project; messaging is the easy part. ## Concepts ``` team ──< person ──< machine ──< session vibes alex alex-mbp api-claude (claude-code, acme/api@feat/auth, busy 2m, "JWT middleware") web-codex (codex, acme/web@main, idle 14m) alice alice-xps api-cursor ... ``` - **Team**: a named group with a membership roster. Joined once per machine with an invite. - **Person**: a human. Owns one or more machines. Not an address: messages go to agent sessions only. - **Machine**: one daemon, one iroh key. Identity = `EndpointId` (ed25519 public key). - **Session**: one live agent conversation. Has a name, agent kind, cwd, repo, branch, status, a one-line `doing`, and a delivery tier (below). ### Addresses | Address | Means | |---|---| | `alice/api-cursor` | one session | | `alice/api-cursor@xps` | that session on machine `xps` (when alice has two sessions of that name) | | `api-cursor` | a session, when the name is unique in the team | | `alice/*` | every live session of alice's | | `repo:acme/api` or `repo:api` | every live session working in that repo | | `*` | everyone | The groups (`alice/*`, `repo:`, `*`, with or without `@machine`) are for a person at a terminal: agents message one session. Session names default to `-` (`api-claude`), get `-2`, `-3` on collision, and can be renamed by the agent. A new session in the same place with the same agent **inherits a stale name and its unread mail**, so "tell the api agent X" survives restarts (idea from agentbus). ## Architecture ``` ┌─ machine: alex-mbp ───────────────────────────────────────────┐ │ claude ── reason-network mcp ──┐ │ │ codex ── reason-network mcp ──┤ unix socket ┌──────────────┐ │ iroh QUIC (ed25519-authenticated) │ cursor ── reason-network mcp ──┼──────────────────▶ │ Reason Network daemon │◀──┼──▶ direct on LAN (mDNS) │ hooks: reason-network hook … ──┘ ~/.reason/network/sock │ SQLite │ │ hole-punched over internet │ you: reason-network who/send ──────────────────▶ └──────────────┘ │ relay fallback over HTTPS :443 └───────────────────────────────────────────────────────────────┘ ``` One static Rust binary, `reason-network` (fronted by `reason network` where the Reason CLI is installed), with subcommands: - `reason-network daemon`: per machine. Owns the iroh endpoint, roster, directory, mailboxes (SQLite). Started on demand by any other subcommand, or installed as a launchd/systemd user service (on Windows, started at logon from the user's Run key; there the local socket is a named pipe). - `reason-network mcp`: stdio MCP server spawned by the agent. Thin client of the daemon. **Must handle many sessions per process**: Codex, opencode and Cursor share one MCP process across threads/tabs. - `reason-network hook `: hook handler (status, context injection, stop-continuation, wake). - `reason-network who | send | listen | status | init | invite | join | setup | doctor`: the CLI. `send` goes out as the agent session whose shell runs it, `--from `, or from a plain terminal as `/terminal`; `listen --as ` waits for the next message to `/`. Why Rust: iroh 1.x (QUIC dialed by public key, hole punching, HTTPS relays, mDNS) and rmcp (official MCP SDK) both exist and work today. Zig has neither, which is why agent-rpc's MCP shim had to be TypeScript. ## Network layer (M1) **Transport: iroh 1.3.** Each daemon is an iroh `Endpoint` with a persistent secret key (`~/.reason/network/machine.key`). Peers are dialed by `EndpointId`; iroh resolves addresses via n0 DNS/pkarr, finds LAN peers with `iroh-mdns-address-lookup` (works with no internet), hole-punches, and falls back to a relay over HTTPS. Relays only see encrypted QUIC. The default relays are Reason's, one per region, as Reason lists them at `https://reasonmachines.com/network/relays.json` (`src/relays.rs`), as n0's public relays are rate-limited and meant for development. Each machine makes the one with the lowest latency its home relay; relays don't talk to each other, but iroh reaches a peer through the peer's home relay, so machines homed on different ones still find each other. The list steers relay traffic (end-to-end encrypted, but a bad list could cut machines off or show who talks to whom), so it is taken only signed with the same key as sign-in proofs (its own `typ`, `reason-network-relays`, and `iss`), fetched only over HTTPS from that address without following redirects, and rejected whole on any fault (an http relay, a duplicate, more than 16, over 64 KiB, expired). The daemon fetches it at start (waiting briefly for it on a machine's first network start) and every 6 hours, keeps the last good one in the state folder (`relays.json`, used even once expired while no newer comes), and applies a new one at the next network start. Without any, sfo1 (`DEFAULT_RELAYS` in `src/net.rs`). A team without its own relay still looks addresses up on n0's DNS, where each machine publishes its home relay: that is how a machine on Reason's relay and one of an older build, on n0's, find each other (iroh dials a peer through whichever relay it is on), and what a four-word code needs. Teammates are also dialed through each of the default relays, so machines of this build find each other even without that lookup. A team can set its own relay (`reason-network relay ` or `reason-network init --relay `; the `iroh-relay` binary on any VPS, Let's Encrypt on :443, access limited to the roster's endpoint ids). Then its machines never contact n0: they use only that relay (teammates are dialed through it) and mDNS on the LAN, and invites come in the long form, since a four-word code needs n0's lookup. A machine that was off while the team switched relays can't find it on its own; `reason-network doctor` there says to set the same relay with `reason-network relay`. That is the "HTTPS and local network, MagicDNS style" requirement with one stack, and a relay as the only server (Reason's, or the team's own). **Roster (membership).** Replicated to every machine, every entry signed by a member machine's key: - *admissions*: how each machine got in (the founder's self-signed entry, then each admission signed by an already-admitted machine). Kept forever, so who may sign never depends on the order changes arrive in, and all machines converge on the same roster. - *members*: each machine's latest entry (person, machine name, active or removed), last writer wins. - *invites*: `hash(secret)`, expiry, optional "for this person's own machine", and who used it. - *meta*: team name and optional relay. Any member may invite and remove. No machine can admit itself; it can sign its own departure. Invites die with the machine that made them and are single use. Entries stamped more than a day ahead are refused. A removed machine can no longer connect, but it is not made cryptographically powerless: this is for friends. **Gate.** Incoming connections on the team protocol are dropped unless the peer is an active member. A machine that was removed while away is first sent the state that says so; strangers learn nothing. **Invites.** The main way in is the **team link**: `https:///join/#`, where the ticket packs the team id, the team link's secret, the inviting machine's relay and LAN address hints, and up to six members' keys. The link secret is a reusable invite in the team state (any member can show the link and any online member can redeem it), replaced with `reason-network link --new` and whenever someone is removed; removed machines are never sent it. It travels as the invite line, `Join the Reason Network: `, which the joiner pastes to their agent: they sign in with Reason, and their computer joins with a sign-in proof (see below). Four-word one-time codes (`reason-network invite --code`, ~41 bits) derive a temporary iroh key the inviting machine listens on, for joining without signing in. A code can't show the joiner which team answers it (whoever listens on the code's key knows the code, so could make up a team), so a sign-in never joins with one: only with a link, which names the team. Links are `rn1-…` (older builds wrote `tincan1…`, with the same payload; both are read). After `init` and `join` the CLI prints the invite line, `Join the Reason Network: `, and agents get it from `list_team`. **Sign-in proofs.** Signed in with Reason, a machine carries a proof: a compact JWS from Reason's cloud-access issuer (`iss` `https://reasonmachines.com/network`), RS256 only, saying this endpoint id belongs to account `sub`, named `name`, issued `iat`, until `exp` (`init`/`join --proof`, `reason-network proof` to refresh). The engine checks it against Reason's RSA public keys (2048 bits or more), compiled in and never removed, and never calls Reason; debug builds also trust the keys in `REASON_NETWORK_TRUST_KEYS` (an RSA JWKS), for tests, and release builds don't. A proof longer than 4 KB (one signed with a 4096-bit key is at most about 1.6 KB), or issued more than a day ahead, is refused. Proofs travel in the team state's `attestations` (endpoint → JWS), outside the signed entries, so builds without them pass the rest on unchanged; each machine checks every proof itself, drops the ones that don't verify, and keeps them only for members. `verified` is per person: a person shows as verified while a machine of theirs has a proof for their name that hasn't expired. `has_proof` is per machine. Expiry decides only that mark, never membership: who may hold a name goes by Reason's signature alone, so it doesn't change as time passes or differ between machines' clocks. With a proof, the name is the proof's. A name held with a proof can't be taken by anyone else (`{"error":"name_taken"}` with `--json`, for the Reason CLI, which signs people in). Another computer of the same account may take it: the joiner first asks the link's machines for one of that person's own, whose word every build takes (a machine that isn't says `same_account_needs_owner` and the joiner asks the next); if none of them is online, a machine of this build lets it in on its proof; if none can, the join fails with `{"error":"same_account_needs_owner"}`, never `name_taken`. Builds before proofs (0.10.x) never see a computer let in on its proof, only one let in by the person's own machine. If every machine asked is that old, nothing tells the joiner it's the same account, and the join comes back as `name_taken`. **Presence and directory.** Daemons keep a connection to every online member (full mesh; fine to ~50 machines). On connect they exchange roster heads (anti-entropy) and full presence. On any local change a daemon pushes `Presence{machine, seq, sessions[], human_status}` to all peers. A dropped connection marks that machine offline (sessions shown with last-seen). **Messages.** `Envelope{id: ulid, from, to, reply_to, thread, body, sent_at}` sent directly to each target machine on ALPN `tincan/1`, length-prefixed JSON frames. The receiver persists, then acks with `stored | delivered | held | refused`; later receipts (`delivered`, `read`) flow back. The sender keeps an outbox and retries when the target machine comes back. Mail for an offline session waits in the target machine's mailbox under the session name, for a day: a session gone longer is forgotten, with the mail it never read (not the person's own `listen` mailboxes, such as `terminal`, nor mail held for them; and nothing in the hours after the daemon starts, while live sessions register again). **Files.** A message can carry small text files (`files: [{name, content}]`, at most 10, 256 KB each, 512 KB in all; UTF-8 with no control characters but tab and line ends), inside the same envelope, so they are as authenticated as its text. The sender's client reads them from paths on its computer: a person's `send --file` from anywhere, an agent's `send_message` only from its own folder (its repository, or where it works; links resolved), none of that folder's hidden files or folders, and nothing under `~/.config`, `~/.ssh`, `~/.aws`, `~/Library`, `%APPDATA%`, `%LOCALAPPDATA%` or the state home (a session working in the home folder or above it sends none), so a prompt-injected agent can't mail out what its harness wouldn't let it read. Both daemons hold them to the limits and reduce each name to a plain file name (no separators or `..`, no leading dot or dash, nothing invisible, no Windows device names such as `CON`, `COM¹` or `CONIN$`, at most 100 characters and 200 bytes, distinct within the message, defanged like message text). A peer's message id must be a ULID as written. The receiver saves the files in a new folder of its own, `/inbox//` (named by an id made there, never the sender's: folder 0700, files 0600, never executable, never opened, marked as downloaded with macOS's quarantine or Windows' internet zone; a name the file system folds into one already there gets `-2`), and lists their paths after the message's frame, where its text can't imitate them. A held message's files are saved only once it is approved (one that can't be saved stays held); a refused one's never are. They go with their message: when it is forgotten, a day after it was read, or a week after they came while it is unread; a message held a week with files goes. A machine keeps at most 100 MB of teammates' files (saved and held) keeps at most 25 MB from one person, and takes at most 20 MB an hour from one person; past either, the sender's machine is told to retry later and keeps the message in its outbox. A machine's presence lists `features: ["files"]` when its engine takes them; an older engine would drop the field without a word, so for a machine that doesn't say so, the files are written out in the text as fenced blocks when that fits the 64 KB body limit, and the message is refused for that recipient otherwise. Later (M4): an **anchor** is just a member daemon on an always-on box (VPS, Pi) with `role=anchor` that also holds mail for offline machines and keeps searchable team history. Same binary, no special server. ## Local layer (M0) **Daemon API**: newline-delimited JSON over `~/.reason/network/network.sock` (mode 0600). Requests carry an `id`; a session connection also receives pushed `deliver` events for its sessions. **Session registration**, by priority: 1. Per-call `_meta`: Codex `sessionId` (the root thread, same id its hooks see), opencode V2 `ai.opencode/sessionID`. 2. Env: `CLAUDE_CODE_SESSION_ID` (Claude), `COPILOT_AGENT_SESSION_ID` (Copilot), `GROK_SESSION_ID` (Grok). 3. Hook stdin `session_id` (`SessionStart`), correlated to the MCP process by agent PID ancestry. 4. Fallback: one session per MCP process. Agent kind comes from MCP `clientInfo.name` plus env (`CLAUDECODE`, `CODEX_*`, `GEMINI_CLI`, `CURSOR_AGENT`, `OPENCODE`, `COPILOT_CLI`, `GROK_SESSION_ID`). **Passive discovery.** Claude Code writes every live session to `~/.claude/sessions/.json` (name, cwd, busy/idle). The daemon reads these, so Claude sessions show up with live busy/idle status even without our MCP server loaded. ## Getting messages into agents The hard part. Each session advertises a **delivery tier** so senders know when it will be seen: - **push**: injected mid-turn and **wakes an idle session**. - **turn**: injected at the next tool call or at stop, but an idle session waits for its human. - **pull**: only when the agent checks (`check_inbox`, or the unread notice appended to every tool result). | Agent | Tier | Mechanism | |---|---|---| | Claude Code | push | MCP child posts to the session's own inbox socket (`CLAUDE_CODE_MESSAGING_SOCKET`, `{"type":"user",…}` frame): delivered between tool calls, starts a turn if idle. Fallbacks: `asyncRewake` hook (documented idle wake), channels (`notifications/claude/channel`, needs a dev flag until allowlisted). | | Codex | push | `codex queue --thread ` / app-server `turn/start` (default daemon mode). Mid-turn: `PostToolUse` `additionalContext`. Stop: `decision:block`. Hooks need one-time approval in `/hooks`. | | opencode | push | Plugin (one file for 1.x and 2.x): `client.session.promptAsync` (1.x), `ctx.session.prompt` (2.x, where busy/idle come from `session.execution.*` events). | | pi | push | Extension (`~/.pi/agent/extensions/reason-network.ts`): registers the MCP server per session (`deferred`, with `REASON_NETWORK_SESSION`), `sendUserMessage` (`deliverAs: "steer"` mid-run, a new turn when idle), busy/idle from `agent_start`/`agent_settled`, the session name as `doing`. | | Reason | push | Its agent runs in Reason's cloud: the daemon lists live sessions (`reason session list --json`, when `reason = true`) and sends with `reason session send`. Replies go through the CLI: `reason-network send --from `. | | Amp | push | Plugin: `thread.appendUserMessage(…, {steer:true})`. | | Cursor | turn | `postToolUse` `additional_context`; `stop` `followup_message`. Idle: background `reason-network wait`. | | Gemini CLI | turn | `AfterTool` `additionalContext`; `AfterAgent` block. Idle: experimental background-shell inject. | | Copilot CLI | turn | `postToolUse` `additionalContext`; `agentStop` block; `notification` hook. | | Grok CLI | turn | `PostToolUse` `additionalContext`; `Stop` (`end_turn`) block. SessionStart and prompt-submit output are dropped; subagents' hooks are ignored. | | DeepSeek Harness | pull | MCP only: `setup` adds our server to `$DSH_HOME/cordis.patch.yml`, which dsh applies to every profile. | | any MCP client | pull | Tools + unread notice on every tool result + `check_inbox(wait_secs)` long-poll. | MCP notifications are not a delivery path: no client passes them to the model (checked Oct 2026). Gotcha: Cursor, Copilot CLI and Devin load **Claude Code's** hooks from `~/.claude/settings.json`, so `reason-network hook` must detect which agent is actually calling it. ## What the agent sees Three tools, none of which an agent has to call unless it wants to message someone: - `list_team(filter?, all?)`: the directory, grouped by person then machine. One line per session: name, agent, status and for how long, repo@branch, `doing`, tier. Marks `← you`. - `send_message(to, message, reply_to?, notify?, files?)`: `files` are paths of small text files in the session's own folder to carry (see Files above). Returns per-recipient state (`delivered` / `queued` / `held`) and tier, e.g. "queued: alice/web-cursor is idle and turn-only, it sees this when alice next prompts it". (`reason-network send --fyi` doesn't wake: read at the next natural pause.) `notify` also pops it up on the person's screen, for an approval or decision only they can make: osascript on macOS, notify-send on Linux, a PowerShell toast on Windows, at most one per person sending a minute, none for messages over 15 minutes old; a machine without a desktop says so. With `notify`, `to` may be your own user: then only the pop-up, on each of your machines that is on (an agent that needs its user). - `check_inbox(wait_secs?)`: unread messages; can block waiting for a reply. - `ask_user(question, about?)`: asks the session's own person with a pop-up on its machine and waits (up to 10 minutes) for Approve or Deny, with a note: on macOS and Windows a floating panel that doesn't take the keyboard (Return approves once it is clicked into, Escape denies), on Linux a notification with buttons (no note; libnotify 0.7.10+). The answer is the tool's result, not a message: only the pop-up on that machine gives it. One question per session is up at a time (asking it again waits for it, another is refused until then); it stays up an hour, and an answer after the call gave up comes as an ordinary message from `/terminal`. No desktop: an error, so the agent asks in its own session. With `about` (the id of a teammate's message to this session), the pop-up shows that request first, in its sender's words (up to 500 characters, marked where cut), then the agent's question in the room left (a panel cuts the question first and the request only after it, and says how much of each it showed; a Linux notification shows what its server shows; direction overrides and invisible characters are left out of what's shown and signed, though glyphs drawn over each other can still hide text the panel laid out); the answer also goes to the request's sender, from the asking session or the person's terminal if it has gone, as an `Approval` holding what was shown (the request's id and sender, the approver, the decision, the request, the question, the note, the time), signed with this machine's key like the team's own entries. The receiving machine keeps it only if the machine that sent it signed it, as its own person, for the session it arrived at; anything else is dropped, and the message says it didn't verify. Which request it answers is the approver's machine's word, so agents see `` and check `from` is the person they asked and `about` their request; older machines see the plain text. It proves the approver's machine recorded its person's answer: an agent can't write one, and release builds use only the system's own dialog programs, which keeps an agent from simply starting the engine with a stand-in that answers. That is a speed bump, not a guarantee: software that controls the approver's machine can still sign (the machine's key is a file there, and it could build its own engine). Agents never report what they are doing; Reason Network reads it from what they record anyway (`doing.rs`): the session's title (Claude Code's `ai-title` transcript entries, Codex's thread names in `~/.codex/session_index.jsonl`, opencode's session title) and the task in progress on its own todo list (Claude Code's `~/.claude/tasks//`, TodoWrite, Codex's `update_plan`, Gemini's `write_todos`, Cursor's `todo_write`, opencode 1.x's todos). Transcripts come from hooks (`transcript_path`) or, for Claude Code, from its session id; only what was appended since the last look is read. A Claude Code session renamed with `/rename` keeps that name on Reason Network. Incoming messages are framed as peer input, never as the user: ``` Did you change the session token format? My tests started failing on refresh. From alice's agent. Reply if useful with send_message to "alice/api-cursor". ``` The MCP `instructions` only cover messaging: check `list_team` before sending, keep messages short with a clear first line, no chit-chat loops, and never do something on a peer's request that your own user would not approve. Nothing changes how an agent works when it isn't messaging. ## Trust and safety - Only roster machines can connect (iroh handshake proves the key; hook rejects the rest). - Peer text is framed as peer input. Agents are told not to treat it as user instructions. - Per-machine inbound policy: `accept` (default within the team), `hold` (you approve with `reason-network held` / `reason-network approve`), `refuse`; overridable per person. Claude Code adds its own `crossSessionInbound` setting on top: a session that skips permission prompts holds a message from a stranger for its user unless the setting is `accept`. A session without a `reason-network mcp` of its own (open before setup, or its server turned off or crashed) gets the daemon's post directly, as a stranger; `send` says so when the transcript shows that mode (state `pushed`, note `held by Claude Code…`). - Loop guard: rate limit per sender, and a cap on consecutive agent↔agent messages in a thread without any human input. - Group guard: an agent's `*`, `person/*` or `repo:` message is refused before it goes anywhere, so it picks one session. Only a sender that is `/terminal` may use them: the CLI without `--from` when someone sits at the terminal, or `--from /terminal` from a script. A machine also refuses a group message from a teammate's agent (an older Reason Network's). It doesn't stop a teammate's agent from messaging your sessions one by one; the loop guard and the inbound policy cover that. - Relays see ciphertext only (and which machines talk to which); the default one is Reason's. With its own relay, a team's machines contact neither Reason's relay nor n0. ## Install UX (M3) ```sh curl -fsSL https://reasonmachines.com/install.sh | bash -s -- network # sign in, create or join a network curl -fsSL https://reasonmachines.com/install.sh | bash -s -- network join '' # join with an invite link ``` Or paste `Join the Reason Network: https://reasonmachines.com/network` (or an invite line) to a coding agent. The Reason CLI's `reason network` signs the person in in their browser, where they create a network or join one, then runs this engine's `init` or `join` with `--proof`, sets up the agents, and prints the invite line. Hosted assistants add the MCP server `https://mcp.reasonmachines.com/mcp`. On its own, without signing in (names unverified), the engine still installs from this repo's releases and runs `reason-network init --as ` or `reason-network join '' --as `. `join`/`init` run `setup`, which detects installed agents and wires each one (backing up every file it edits): Claude Code plugin or `claude mcp add --scope user` + hooks, `~/.codex/config.toml` + `hooks.json`, `~/.cursor/mcp.json` + `hooks.json`, `~/.gemini/settings.json`, opencode plugin + MCP, Copilot `mcp-config.json` + hooks, `~/.grok/config.toml` + `hooks/reason-network.json`. Or paste the invite line into any agent. ## Milestones - **M0, one machine** (done): daemon, socket API, SQLite, MCP server with the four tools, CLI, Claude push delivery, passive Claude discovery, pull delivery for everything else. - **M1, the team** (done): iroh endpoint, signed roster, `init`/`invite`/`join`/`remove`/`leave`, presence sync, routing with acks and outbox, hold/refuse policies, rate limit, mDNS, custom relay. - **M2, adapters** (done): Codex (`codex queue` + hooks), Cursor, Gemini, Copilot (hooks), opencode (plugin); `setup [--remove]`, launchd/systemd service, `doctor`. Amp: MCP only, by hand. - **M3, ship** (done): GitHub Actions builds for macOS, Linux and Windows; `install.sh` / `install.ps1` (install and join in one line, or hand over to the Reason CLI), with the release. Standalone installs update themselves; the Reason CLI pins its own. Later: Homebrew. - **M4, team features**: file-overlap warnings ("bob/api-codex is also editing src/auth.rs", from `PostToolUse` edit paths), anchor mailbox and history, hold/approve notifications, web dashboard. ## Defaults (easy to change) - Names: `reason-network` for the binary and the repo, Reason Network in prose. - License: MIT OR Apache-2.0. - Inbound policy between teammates: `accept` (per-person `hold`/`refuse` available). - Teammates see repo@branch and the last component of the working directory; full paths are opt-in. ## Prior art [agentbus](https://github.com/luqs1/agentbus) (Rust, iroh, no license), [agents-party](https://github.com/1gr14/agents-party), [agent-rpc](https://github.com/currenthandle/agent-rpc) (Zig broker + tmux injection), [mcp_agent_mail](https://github.com/Dicklesworthstone/mcp_agent_mail), [agent-mesh](https://github.com/satja/agent-mesh) (Codex app-server delivery, `peek_peer`), [agent-peer](https://github.com/mkhuda/agent-peer), [ccbridge](https://github.com/sourabhnirvani/ccbridge), [agent-bridge](https://github.com/trananhhh/agent-bridge), [intermcp](https://github.com/Guo-astro/intermcp), [claude-message-mcp](https://github.com/aeriondyseti/claude-message-mcp), [Paseo](https://paseo.sh). ## Config `~/.reason/network/config.toml`: | Key | Default | | |---|---|---| | `person` | `$USER` | your name (set by `init`/`join`) | | `share_paths` | `false` | show teammates full working directories | | `share_unmanaged` | `false` | show teammates Claude Code sessions that don't have Reason Network loaded | | `tray` | `true` | macOS: show the menu bar icon while the background service runs | | `lan` | `true` | find teammates on the local network (mDNS) | | `auto_update` | `true` | install new releases when they come out (checked every few hours) | | `reason` | `false` | list your live Reason sessions (set by `reason-network setup`) | | `inbound` | accept | see above | | `folders` | none | folders whose sessions are never shared, e.g. `"~/personal" = "off"` (`reason-network folder off`, `on` undoes it) | A session in a folder that is off (by its own folder or any other its agent names, links followed) never registers: its tools say its user turned Reason Network off for the folder, its hooks print nothing, and its agent sends no files from there. The person's own terminal isn't held to it (an agent's shell is). Agents that share one MCP process across sessions (Cursor, opencode, Codex's app-server) don't tell it which folder a call comes from: the session's hooks mark it off by its id, and while one of them is mid-turn (until its turn ends, at most 10 minutes after its last prompt or tool call), every call of that agent that names no session, from folders that are on too, is refused with "try again in a few minutes", never handed another session's identity. Known limit: a session of such an agent whose hooks don't run (Codex before `/hooks` trusts them) is judged by the MCP process's folder. ## Troubleshooting - **`reason-network doctor`** checks the daemon, the team connection (direct or relayed, per machine) and every agent. - **An agent doesn't have the Reason Network tools:** restart that session; agents load MCP servers at startup. - **A teammate shows as offline:** their machine is asleep or Reason Network isn't running there (`reason-network doctor` on their side). Messages wait in your outbox and go out when they're back. If it was off while the team changed relays, run `reason-network relay` there with the team's relay (`reason-network team` shows it). - **macOS asks to let Reason Network find devices on your local network:** allow it for direct LAN connections. If you don't, Reason Network still works over the internet. - **Windows asks to let Reason Network through the firewall:** allow it for direct and LAN connections. If you don't, Reason Network still works, through a relay. - **Codex sessions don't show up until they use a Reason Network tool:** Codex hasn't been told to trust the hooks yet; run `/hooks` in Codex. - Logs: `~/.reason/network/daemon.log`.