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 listThe 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|fileMoves 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 offLocal 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 onPoints 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 offRemoves 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|inheritChanges that answer; the running daemon picks it up within seconds.
sessionpipe control statusReceivers, 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 runThe 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 offsessionpipe update onThe daemon's daily update check, off or back on.
sessionpipe versionControl
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/connectset 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 runitself): 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-scangates it)."*"in an expected event is a wildcard foridandtime. An emptyexpectis 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'senqueuestep 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. Ahellostep posts itsbodyas 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 turnedremote_titleson: claude.ai's name for a Claude Code session (spec/ADAPTERS.md, the Facts row).expect.titleis the name a reader must take, ornullfor none. Read bypackages/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 bypackages/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 bypackages/core/test/control-vectors.test.ts;npm run vectorswrites the ones not yet there and keeps the rest (SESSIONPIPE_WRITE_VECTORS=allrewrites 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.