Get started
Markdownllms.txt

Docs

On this page

Everything the sessionpipe command does, and how the parts that run on their own behave. New here? Get started is the five-minute version; the protocol is the contract a receiver implements.

The usage lines below are read from the CLI's own --help table when the site is built, so they are the ones your copy prints (sessionpipe <command> --help). The longer sections are the package READMEs, included as they are in the repository.

Commands

Set up

sessionpipe connect <receiver> [--machine NAME] [--rename-machine] [--allow-downgrade] [--tier 0-3] [--mode safe|auto|inherit] [--folder DIR]… [--status-line|--no-status-line] [--no-service]

The whole setup against a receiver that offers pairing, in one command and one approval. It shows six digits; approve them on any device where you're signed in to the receiver. You get hooks for every agent and every Claude Code account here, a sink at tier 2, signed messages, keys in the keychain, a daemon that outlives logout, and the last 30 days backfilled. Run it again and it only fixes what's missing.

sessionpipe install [--<harness>] [--machine NAME] [--rename-machine] [--allow-downgrade] [--backfill DAYS] [--sink URL --tier N] [--status-line|--no-status-line]

Finds the agents on this machine, writes their hooks and backfills 30 days into the local outbox. Nothing is sent until a sink exists. --<harness> hooks one even if it isn't detected; --sink URL --tier N adds a sink first.

sessionpipe uninstall [--keep-state]

Removes the hook entries sessionpipe wrote and nothing else: every config file is otherwise byte-identical. The outbox stays under the state folder and is pruned after 30 days.

Destinations

sessionpipe sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--secret S] [--pii] [--name N]

Adds a destination: an HTTPS receiver, a file, or stdout. For a receiver it reads /.well-known/sessionpipe, caps the tier at the receiver's max_tier, stores the token and sends one heartbeat as a test. --pii turns on the PII pass for this sink.

sessionpipe sink list

The sinks on this machine.

sessionpipe sink remove <name>

Removes one.

sessionpipe sink test <name>

Sends a heartbeat and then its session.forgotten, so the receiver keeps no phantom session from the test.

sessionpipe secrets [move keychain|secret-service|file]

Where each key on this machine lives (the macOS Keychain, a desktop session's Secret Service keyring, or a 0600 file), and the best store this session can reach.

sessionpipe secrets move keychain|secret-service|file

Moves sink and control keys to that store. A key it can't read here, or one the store refuses, stays where it is, and the command says which.

Watch and check

sessionpipe tail [--session ID] [--tier 0-3] [--harness NAME] [--all] [--json]

Follows the local outbox, one event per line, filtered to a tier (1 unless you say): exactly what a sink at that tier would receive. No server needed. --all starts from the beginning of each session instead of now; --json prints the filtered events whole.

sessionpipe status [--json]

Sinks and what each has waiting, queued jobs, hook timing (p50 and p95 of the last 100) and the self-update state. --json is described below.

sessionpipe doctor [--json]

Everything status checks, plus a live hook benchmark, each Claude Code account's sign-in, keychain access, and warnings for what looks wrong.

History

sessionpipe backfill [--days 30] [--<harness>]

Reads the last N days of sessions from the agents' own files into the outbox.

sessionpipe replay [--sink NAME] [--from-start]

Sends what's waiting to a sink now, without waiting out a backoff. --from-start rewinds the sink's cursor first, so everything in the outbox goes again.

sessionpipe forget <harness> <session>

Asks every sink to delete one session (a session.forgotten event), then removes it from the local outbox.

Session names

sessionpipe titles [on|off|refresh] [--days 30]

Which one holds. A session the Claude app starts here is named by claude.ai a few seconds after its first message, and that name never reaches the machine; out of the box sessionpipe reads local files only, so such a session goes out under your first prompt.

sessionpipe titles on [--days 30]

Claude Code sessions with a claude.ai link take claude.ai's name, read with the sign-in Claude Code keeps and sent to api.anthropic.com alone. It also asks about the last N days' sessions and renames the ones a receiver already has. A name you gave the session (/rename) still wins.

sessionpipe titles refresh [--days 30]

Asks again about the last N days' sessions.

sessionpipe titles off

Local files only again (the default).

Accounts

sessionpipe pool [--json]

The account pool: the accounts in order, the room each has left in its 5-hour and weekly windows, and when each resets. Nothing here runs until you ask for it; install and connect never touch the pool.

sessionpipe pool add --import | --login [--code -]

--import copies every Claude Code login on this machine into the pool; --login signs in to another account (a URL, then the code to --code -). Credentials live in sessionpipe's own secrets store and never appear on an event or on the terminal.

sessionpipe pool on

Points every Claude Code on this machine at the pool through the env block of its settings.json, and installs the daemon's service if it isn't there. New sessions use the pool from the moment it returns; running ones at their next start.

sessionpipe pool off

Removes exactly what pool on wrote; the accounts stay.

sessionpipe pool order <id|email>…

The complete order: each request goes to the first account that has room.

sessionpipe pool enable <id|email>
sessionpipe pool disable <id|email>

Takes an account out of the rotation, or puts it back, without forgetting it.

sessionpipe pool remove <id|email>

Forgets the account and its credentials.

sessionpipe pool refresh [<id|email>]

Reads usage from the vendor now, for one account or all of them; otherwise the daemon reads it every five minutes while the pool is on.

Messages to your sessions

sessionpipe control pair <receiver> [--folder DIR]… [--mode safe|auto|inherit] [--tier 0-3] [--token T] [--name N] [--no-service]

Pairs this machine with a receiver for signed messages and installs the daemon. Asks once, at this terminal, whether sessions you message run in safe, auto or inherit mode.

sessionpipe control mode auto|safe|inherit

Changes that answer; the running daemon picks it up within seconds.

sessionpipe control status

Receivers, parked waiters, sessions mid-turn, open permission prompts.

sessionpipe control keys [remove <id>]

The device keys enrolled here; remove <id> forgets one.

sessionpipe control keys remove <id>
sessionpipe control off [<receiver>]

Tells the receiver, then forgets its token and keys.

sessionpipe control run

The daemon itself, in the foreground: what launchd or systemd starts. One runs per machine; a second one finds the first, says so and exits 0.

sessionpipe wait [--session <harness>:<id>]

Parks in a session's background so a message reaches it in place, in about 60 ms with no network. Claude Code's session id comes from CLAUDE_CODE_SESSION_ID.

Self-update

sessionpipe update [off|on]

Installs the latest release now; a running daemon restarts onto it once nothing is in hand.

sessionpipe update off
sessionpipe update on

The daemon's daily update check, off or back on.

sessionpipe version

Control

A receiver that lists control can carry a message from your phone to a session on this machine. It is signed on your device and checked here, against a key you enroll at this terminal; the receiver can queue a message but can never forge one (spec).

sessionpipe control pair spacesheep.dev   # one link: approve with your passkey; installs the daemon (this folder by default)
sessionpipe control folders add ~/work/api   # let messages reach sessions in another folder too (no new pairing)
sessionpipe control folders [remove <dir>]   # the list, or stop allowing a folder
sessionpipe control status          # receivers, parked waiters, sessions mid-turn, open permission prompts
sessionpipe control keys [remove <id>]
sessionpipe control off             # tell the receiver, forget its token and keys

Pairing through npx first installs that version globally and runs pairing from that persistent copy. On Homebrew, the daemon uses a matching stable Node link so a Node upgrade does not leave it pointing at a removed Cellar version.

Services paired with older versions are not repaired by install or update. After upgrading, run sessionpipe control pair <receiver> again (without --no-service) to rewrite the service with persistent paths. On Linux, restart an already-running daemon afterward with systemctl --user restart sessionpipe-control.service; macOS pairing reloads the LaunchAgent.

A new session started from the receiver may bring files (screenshots, PDFs: up to 10, 50 MiB together), whichever harness it is for. The daemon downloads each one, checks it against the hash you signed, saves it under <folder>/.sessionpipe/files/<session>/ (a folder that git-ignores itself) and tells the session where they are. If any file is missing or doesn't match, the session doesn't start.

One daemon per computer holds one long-poll per receiver and delivers each message the cheapest way the session allows: to sessionpipe wait parked in the session's background (in place, ~60 ms, no network), at the session's next Stop if it is mid-turn, through the Agent SDK for a session it started (if the SDK is installed), or as a headless claude -p --resume (a --fork-session copy when a live process holds it). A session stays reachable in place with zero idle turns by running, as a background command with the longest timeout it can (Claude Code: raise BASH_MAX_TIMEOUT_MS, else a background command is stopped after 2 hours at most):

sessionpipe wait            # Claude Code: the session id comes from CLAUDE_CODE_SESSION_ID

The waiter and next-Stop modes work in any Claude Code session that runs your hooks, the Claude desktop app's included, and need nothing else. The Agent SDK, resume and fork modes run the claude command, which has its own login (claude auth login); the desktop app doesn't share its login with that command.

Permission prompts can be answered from the receiver too: while the terminal shows its prompt, the PermissionRequest hook waits for your signed answer, and whichever comes first wins. Nothing is ever auto-allowed. Safe mode (the default) runs headless turns with --permission-mode dontAsk, so nothing that needs approval runs while you're away. Auto mode runs them with --permission-mode auto; where Claude Code's settings turn auto mode off (permissions.disableAutoMode), Claude Code runs them in its default mode instead, which refuses whatever needs approval. Inherit mode (sessionpipe control mode inherit, chosen only at the machine) passes no permission flag, so each turn starts in the mode Claude Code picks by itself there, its settings' permissions.defaultMode: messaged sessions run the way your own do, bypassPermissions included if that is what you set.

Codex and Antigravity sessions are reached headlessly too, when codex or agy is installed (the daemon looks on PATH, beside its own Node, in ~/.local/bin, or where SESSIONPIPE_CODEX / SESSIONPIPE_AGY point). A new session is codex exec --json -C <folder> or agy -p … --output-format stream-json; the next message is codex exec resume <thread> or agy --conversation <id>. Safe mode runs Codex in its workspace-write sandbox and agy with no permission flag (it denies, and the reply lists, what it wasn't allowed to do); auto mode passes --approve-for-me / --mode accept-edits; inherit mode passes no flag, so Codex follows the approval_policy and sandbox_mode in your config.toml. The bypass flags are never passed. Both CLIs pick a new session's id themselves, so the daemon keeps the one your start named: it records the pair in ~/.local/state/sessionpipe/control/aliases.json, and your receiver keeps seeing one session under one id. A message to a Codex session someone has open (its rollout written in the last 90 seconds) is acked unsupported rather than resumed beside them. Before a Codex run the daemon checks codex login status (or CODEX_API_KEY) and says codex login --device-auth if it isn't signed in.

Any other agent that speaks the Agent Client Protocol is reached the same way: Gemini CLI, OpenCode, goose, Copilot CLI, Cursor, Droid, Devin, Kilo, Qwen Code, Cline and the rest of the ACP registry (the list, with each one's launch line). The daemon runs the agent's own CLI (gemini --acp, opencode acp, …) when it is installed, on PATH or where SESSIONPIPE_ACP_<HARNESS> points (SESSIONPIPE_ACP_GEMINI_CLI), and never downloads one. It is the agent's client: a start opens a session in your folder, the reply streams back as the agent writes it, and the process stays up so the next message lands in it with no cold start (api mode); cancel works there too. An agent idle for ten minutes is closed, and a later message reopens the session where the agent supports session/load. Safe mode answers every permission the agent asks with a refusal, listed in the reply; auto mode allows. An agent that never asks (OpenCode, by its own settings) runs its tools as it would at your terminal. Signing in is yours to do at the terminal: an agent that wants it says so in the reply.

Updates

The control daemon keeps its own copy current. About a minute after it starts, and then once a day (the last check is kept in ~/.local/state/sessionpipe/control/update.json, so a restart or a machine that sleeps doesn't ask more often), it reads https://registry.npmjs.org/sessionpipe/latest. When that is a newer release it waits until nothing is in hand — no message being delivered, no headless run or new session, no Agent SDK turn, no permission prompt waiting for your answer, no session of yours mid-turn — stops taking new messages (the receiver holds them meanwhile), runs npm install --global --ignore-scripts sessionpipe@<version> into the prefix it was installed in, re-copies the hook launcher's files, and restarts onto the new code. A running turn is never interrupted.

How it restarts depends on how it runs:

  • launchd (macOS) or systemd (Linux), as control pair / connect set it up: the daemon exits with code 75 and the service starts it again. Every service file sessionpipe has written restarts on a non-zero exit, so older installs need nothing.
  • Anything else (a terminal, tmux, a container that starts sessionpipe control run itself): the old process stays as the new daemon's parent, with the same environment and output, and restarts it the same way after each later update. Whatever waits on the daemon (a shell, a container's init) keeps waiting.

One daemon runs per machine: a second sessionpipe control run finds the first one answering on the local socket, says so, and exits 0.

Only a global npm install that this user can write updates itself; a copy in the npx cache, a git checkout or a dev build is left alone. sessionpipe status and sessionpipe doctor say whether it's on, when it last checked and what it found; the daemon's log (~/.local/state/sessionpipe/control/daemon.log under launchd, journalctl --user -u sessionpipe-control.service under systemd) has one line per check and per update.

sessionpipe update off      # the daemon stops checking ("update_check": false in config.json)
sessionpipe update on
sessionpipe update          # install the latest now; the daemon restarts onto it once idle

SESSIONPIPE_NO_UPDATE_CHECK=1 turns it off too, but only where the daemon can see it: a launchd or systemd service doesn't inherit your shell's variables, while sessionpipe update off reaches it (it re-reads the config every ten minutes).

Session names from claude.ai

A session the Claude app starts on this machine gets its name from claude.ai, a few seconds after its first message, and claude.ai never writes that name to the machine. Out of the box sessionpipe reads only local files, so such a session goes out under your first prompt cut to 50 characters while the Claude app shows another name.

sessionpipe titles on       # Claude Code sessions with a claude.ai link take claude.ai's name
sessionpipe titles refresh  # ask again about the last 30 days' sessions (--days N)
sessionpipe titles off      # local files only again (the default)
sessionpipe titles          # which one holds

With it on, the worker asks claude.ai for the name of each Claude Code session that has a claude.ai link, a minute apart until claude.ai has named it, then every half hour. To ask, it reads the sign-in Claude Code keeps for that config dir (the macOS keychain item Claude Code writes, else .credentials.json in the config dir). It sends that token to api.anthropic.com alone and keeps it nowhere: not in a file, an event, a log or a sink. A token about to expire is skipped, never refreshed, so Claude Code's own login is never touched. A name you gave the session here (/rename) still wins, and so does a rename in the Claude app, which reaches the transcript without any of this.

titles on (and titles refresh) also asks about every session of the last 30 days and sends each one a receiver already has under another name again, as session.backfill with claude.ai's name, so finished sessions are renamed too, not only the ones that run again. It prints how many it found named and how many it sent.

The pool

Claude Code can't change account mid-session, but its requests can. sessionpipe pool keeps several Claude accounts behind one loopback listener the control daemon runs on this machine; Claude Code is pointed at it through the env block of its settings.json (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, and the one flag that keeps its first-party features on). Each request goes to the first account in your order that has room, a session stays on its account while that account has room, and when the account's 5-hour or weekly window is used up the next request simply goes to the next account: the session notices nothing. The numbers each account reports arrive as limits.observed with account_id, so a receiver's usage page shows every account (spec, what it reads, privacy).

sessionpipe pool add --import        # copy every Claude Code login on this machine into the pool
sessionpipe pool add --login         # sign in to another account: a URL, then paste the code to --code -
sessionpipe pool on                  # write the env block; installs the daemon's service if it isn't there
sessionpipe pool                     # the accounts in order, each with its room left and reset times
sessionpipe pool order <id|email>…   # the complete order; enable | disable | remove | refresh too
sessionpipe pool off                 # remove exactly what `on` wrote; the accounts stay

Nothing here happens until you run pool add or pool on yourself: install and connect never touch the pool. The credentials you add live in sessionpipe's own secrets store (the keychain where there is one), are sent only to the vendor's own endpoints for your own requests, and never appear on an event, in an ack or on the terminal. The vendor is called (requests forwarded, idle usage read every five minutes) only while the pool is on; pool add --login and pool refresh at the terminal are the exceptions, because you asked. Whether pooling is allowed by each account's plan is yours to check; sessionpipe doesn't change any limit.

New Claude Code sessions use the pool from the moment pool on returns; sessions already running pick it up at their next start, since Claude Code reads settings.json's env when it starts. While the pool is on, every Claude Code request on the machine depends on the daemon: if it isn't running, Claude Code gets "connection refused", and sessionpipe pool says so (sessionpipe control run in a terminal, or the service pool on installs). The daemon runs for the pool alone, with no receiver paired; control off keeps the service while the pool is on. While the daemon runs, every sessionpipe pool command goes to it over the local socket, since it holds the listener, the session pins and the credentials; with no daemon the command runs here and the daemon reads the result when it starts. From a receiver, the same actions arrive as a signed pool command; turning it on or off, removing or reordering accounts, and finishing a sign-in need a fresh confirm with your passkey or device key, never a day grant alone. sessionpipe uninstall runs pool off first.

Status JSON

sessionpipe <command> --help (or -h) prints usage and exits successfully before running the command. This also works after operands and for nested commands, such as sessionpipe sink remove example --help and sessionpipe control pair example --help.

sessionpipe status --json prints one JSON object. Its shared fields use the same names and meanings as doctor --json:

Field Shape / meaning
version, machine, state, config Strings; state and config are absolute paths.
harnesses Object keyed by harness name; each value is an array of {file, state, note?} hook checks. file uses ~ for home, as doctor does. Includes detected or configured harnesses.
sinks Array of {name, url, tier, max_tier, pii, control, paused, pending_sessions}. tier is the configured tier; the effective tier is min(tier, max_tier ?? 3). max_tier and paused are null when unset. No token or secret is included.
timing {n, p50, p95} from the last 100 local hook timings, in milliseconds; null when unavailable.
update Doctor's update summary: {auto, off, last_check, latest, last_error, pending, updated, failed}. auto is boolean; absent details are null.
jobs Number of entries waiting in the local jobs directory; zero when absent.

pending_sessions counts outbox session files with bytes beyond that sink's cursor, including paused sinks; it counts files, not individual events. Empty collections are {} / []. Status omits doctor's active hook benchmark, login/keychain checks, and warnings. Both commands still check hook installation, which can run gemini --version (up to four seconds) when tools.enableHooks is unset. JSON is an output format, not a promise of constant-time diagnostics. Plain sessionpipe status keeps its existing human output.

Conformance

  • harness/<name>/*.json — { input: { argv, stdin, env, files }, expect: [events] }, recorded from real hook payloads (secrets scrubbed; npm run fixture-scan gates it). "*" in an expected event is a wildcard for id and time. An empty expect is a payload the adapter reports nothing for (Codex's ephemeral threads).
  • redaction/secrets.json, redaction/pii.json — [{ in, out }] vectors for the rulesets.
  • short-id.json — { harness, id, short_id } vectors for a session's short id (spec/PROTOCOL.md §3): what a receiver shows and searches by must reach these.
  • delivery/*.json — receiver scenarios: dedup, 413 split, 403 tier, retry, forget, control, pairing renewal. A control scenario's enqueue step is the receiver's own test door: the runner signs the message with a test key it enrolled there, and {run} / {run12} are the run id. A hello step posts its body as the machine's hello (POST {control}/hello, the machine's token), so a scenario can say what the machine advertised before a start.
  • remote/<harness>/*.json — { request, response: { status, body }, expect }, answers recorded from a harness's own service (scrubbed), for the facts an adapter may ask it for once the person turned remote_titles on: claude.ai's name for a Claude Code session (spec/ADAPTERS.md, the Facts row). expect.title is the name a reader must take, or null for none. Read by packages/core/test/claude-code.test.ts.
  • transcripts/<harness>/*.jsonl — transcript files as the harness writes them, scrubbed, for what a reader must keep and skip (spec/ADAPTERS.md, the Transcript row). Read by packages/core/test/transcripts.test.ts.
  • control-vectors/*.json — signed control commands, valid and broken, and enrollment proofs, each with the verdict every verifier must reach (spec/CONTROL.md §5). Run by packages/core/test/control-vectors.test.ts; npm run vectors writes the ones not yet there and keeps the rest (SESSIONPIPE_WRITE_VECTORS=all rewrites every one).

Filled in M1. Run with npm run conformance, or against a live receiver with npx @sessionpipe/conformance https://host.

For agents

Every page on this site is also plain Markdown, so an agent can read it without a browser or a scraper. Hand it the first file below and it can find the rest.

File What it is
/agent.md Context for an agent helping a person with sessionpipe: what it is, every command, and the rules to follow (tiers, approvals, keys). Paste it into Claude Code, Codex or any agent.
/llms.txt The index, in the llms.txt format, linking each page's Markdown.
/llms-full.txt The whole site in one file: this page, the protocol, the changelog.
<page>/index.md Any page's Markdown, such as /docs/index.md. Each page links it in its <head> as rel="alternate" type="text/markdown", and has a button that copies it.
/schema/v1/ JSON Schemas for every message, at the URLs their $ids name.
/changelog/feed.xml Releases, as an Atom feed.

All of it is rebuilt from the repository on every deploy, the same way the pages are, and the Markdown carries absolute links so a copied page still works on its own.