Skip to content

Latest commit

 

History

History
1300 lines (1014 loc) · 103 KB

cli-reference.md

File metadata and controls

1300 lines (1014 loc) · 103 KB

session-peer CLI reference

Source checkpoint 2026-11-02: the v1.4.0 source features below are prepared against published v1.3.3, not an availability announcement. Check the release notes and actual GitHub/PyPI publication before using them.

Back to the overview

Detailed command behavior, installation options, transport limits, and historical validation follow below. Machine consumers should also follow the v1 compatibility contract.

PyPI CI License: MIT Python 3.9+

Message Claude Code and Codex sessions, locally or over SSH, from one CLI. Claude targets use their native inbox socket/pipe; Codex targets use codex queue. SSH runs the same Python script on the destination, so session-peer need not be installed there to receive a send or list request.

This project continues cc-peer with its Git history and issue numbers preserved. session-peer releases are published on PyPI and GitHub; the old PyPI cc-peer project is archived after its final 0.5.1 release. See Moving from cc-peer for explicit migration steps.

Support and security

Public issues are visible to everyone. Share minimal, redacted reproductions; do not attach conversation histories, session databases, authentication files, tokens, or private keys. Emails are reviewed manually and are not automatically published as issues. Support is best effort with no guaranteed response time. English and Korean reports are welcome.

Quick start

Install the CLI with pipx install session-peer or uv tool install session-peer. For the standalone CLI plus Claude skill, see Install.

session-peer list                              # Claude, Codex + registered Antigravity
session-peer list --agent claude                # Claude-only filter
session-peer list --agent codex                 # saved Codex threads
session-peer list --agent codex --host worker   # saved threads on an SSH host

session-peer send --to api-worker --message "message"     # Claude name or PID
session-peer send --to 'codex:<full-thread-uuid>' --message "message" --output-format json
session-peer send --host worker --to 'codex:<full-thread-uuid>' --dry-run -m "message"

Replace <full-thread-uuid> with a full ID from the destination's Codex listing. list includes all registered adapters by default. Antigravity lists only live, explicitly registered bridges. Use --agent codex to filter, not --codex: --codex is not a supported flag and is ambiguous with --codex-home and --codex-bin. send selects the agent from its target, not a --agent flag.

Posted/queued is not acknowledged. Saved Codex threads are not necessarily running. Plain send does not activate a session; explicit --wake is opt-in and does not confirm consumption or a reply.

Delivery explanation (v1.3.0)

Available in v1.3.0. Add --explain to send --dry-run for a human-readable summary of the selected local, SSH or paired-device destination, existing validation result, delivery method and observation limits. doctor --explain describes the existing diagnostic snapshot and declared capabilities, not per-target readiness. handoff status --explain describes an existing intent's stored evidence. Each processed SSH destination receives its own summary; an unexpected error can still stop the existing host loop before later destinations are processed.

session-peer send --host worker --to 'codex:<full-thread-uuid>' --message "Review the patch" --dry-run --explain
session-peer send --device RECEIVER_FINGERPRINT --to review --message "Review the patch" --dry-run --explain
session-peer doctor --explain
session-peer handoff status --correlation-id PREPARED_UUID --explain

The explanation reuses the selected command's existing results and adds no diagnostic, native-version or SSH probes, target activation or standalone list work. The dry-run itself may already run version checks. A later send repeats validation; a successful preview does not reserve the destination or guarantee submission, consumption or completion. Declared support, observed checks and unknown facts are shown separately. Without --wake, wake eligibility is not checked. With it, only the existing preflight result is described.

Paired dry-run keeps the existing receiver-policy resolve operation. The existing connection probe negotiates optional explanation context only with a receiver advertising support. This internal context is versioned, allowlisted and limited to 8 KiB; it adds no general JSON output or handoff schema. The selected receiver agent, target and home are shown only when reported and validated, never guessed from the sender's profile. Older receivers retain their independently known native results while unreported context stays unknown; no fallback, extra probe or resend is attempted.

Handoff status reuses its read-only snapshot without creating an intent, receipt capability, wait or collector, or writing the ledger. Submission, injection observation, explicit receipt and latest stored wait remain separate; even an acknowledged receipt does not prove task completion. Stored observations may be stale. Only a reported receipt observation time is described as such; a description's generation time or diagnostic snapshot is not a delivery observation time. Ordinary sends do not gain receipt ACK support.

This scope is text-only: combining --explain with --json or --output-format json is a usage error. Only send --explain requires --dry-run; doctor --explain and handoff status --explain do not. Send still refuses experimental handoff --correlation-id, --request-ack, --observe-delivery and --wait-for with --explain, before message input or destination/ledger work. Existing commands without the new option keep their output, JSON and exit contracts; explanation does not turn a successful status query into proof of ACK or change the underlying command's exit meaning.

For send, the normal message requirement and limits still apply: supply --message, --message-file or stdin as usual; no placeholder body is generated. Explanation fields do not include the body or receipt secrets. Normal explanations use stdout; failed results or unexpected submission evidence use stderr, without moving existing summary/error output. Unknown evidence stays unknown, and an uncertain actual send must never trigger automatic resend.

v1.4.0 source: structured send preview (#357)

send --dry-run --explain-json opts into a versioned, allowlisted JSON explanation for local or SSH validation. It uses the same classification as the text preview and the native response before SSH adapter shaping. Normal message inputs and limits apply; no placeholder body is generated. The output excludes message contents, raw errors and paths. Unknown or unexpected submission evidence fails validation and does not authorize automatic retry.

--explain-json may be combined with --json, but requires --dry-run and rejects --explain, paired-device and experimental handoff modes before message input or destination work. Existing --explain stays text-only, and ordinary CLI JSON remains unchanged. Existing dry-run probes may start local/version processes or SSH connections, but this mode does not enqueue, post, start a model, reserve wake budget or create handoff authority. A passing preview is a point-in-time check, not approval, a reservation, later-send safety, consumption or ACK.

Optional paired devices and Antigravity (v0.9)

These features are available starting with PyPI v0.9.0. Antigravity remains experimental and the paired transport remains beta; operational validation is still required. Install the extra with pipx install 'session-peer[relay]'. The [relay] extra (Unix, Python 3.11+) enables authenticated paired-device delivery directly or through a self-hosted WSS relay. Pairing pins device identities; a separate operator policy permits individual agent targets and operations. The blind relay cannot decrypt application messages. Follow the paired-device setup and operations guide before exposing an endpoint. The beta does not provide NAT traversal or a hosted public service.

Antigravity requires an explicitly started bridge inside the existing TUI; see Antigravity setup. It is opt-in and does not change Claude/Codex discovery; live Antigravity registrations also appear in unfiltered listing. Normal local and SSH commands retain their standard-library-only installation path. See the v0.9 release notes.

Message input and result output

Optional inbox generation precondition (development)

list --with-target-generation --json adds targetGeneration and generationStatus to each row. For Claude inboxes with native process-creation evidence, copy the opaque tg1: token into send --target-generation TOKEN (including --dry-run or SSH). Keep the token with the original destination; it is a precondition, not a secret, permission, ACK, or retry/deduplication key. Unpinned commands keep their existing behavior and output.

Pinned Claude sends recheck the registry, process creation and inbox endpoint, then verify the connected socket/pipe server PID before writing any message or Windows authentication line. Reused names/PIDs or changed endpoints are refused with stale_target; unprovable native identity is refused rather than guessed. The token does not authenticate the model or defend against a malicious process with the same OS account. OS process creation and connected-peer APIs are needed; unsupported platforms cannot use this mode. Same-generation process state/name updates do not by themselves invalidate the token. Native creation values have OS-specific granularity (Linux uses boot ID and start-time clock ticks); this is not a globally unique or clone-proof identity. The opaque token is also not cryptographic SSH host-key continuity.

A stale_target refusal includes lastSeenTarget with the original requested Claude generation, not a fresh lookup of the successor. This bounded metadata does not include process creation values, paths, user names or host names:

{"status":"refused","reason":"stale_target","submitted":false,"retryAllowed":false,"lastSeenTarget":{"agent":"claude","targetGeneration":"tg1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}

target_generation_unavailable means native identity could not be verified; unsupported_target_generation means the target does not support this precondition. Neither result includes lastSeenTarget or authorizes a retry. The source-streamed restart regression uses an owned protocol inbox and a local SSH stand-in; it is not evidence of a physical remote host or native agent test.

Codex queue targets, Antigravity bridges and paired devices do not acquire this inbox-incarnation guarantee: they report an unsupported generation, and a generic pinned send to them is refused before queue/wake. In particular, a Codex writer recheck cannot atomically constrain which later writer consumes a persisted queue row. Their existing separate identity contracts remain intact. The broader cross-agent work in #180 remains open; this development feature is not shipped in v1.0.4 and adds no automatic fallback or resubmission.

The 2026-10-15 review of the queued-input paths in native Codex 0.162.1 did not identify a supported precondition that atomically checks the original writer incarnation. In the inspected source, the lock-acquisition path does not write an incarnation token, a thread's sessionId survives resume, and thread/queue/add accepts a thread, input and a client message ID rather than a generation precondition. The inspected turn/steer path checks expectedTurnId under the active-turn lock; session-peer does not use that turn-specific check as a writer generation. These findings are limited to the pinned paths, not a proof about every native API or future version. Writer PID, start-time and lock rechecks, queue IDs, canAcceptDirectInput and sessionId do not establish the required original-incarnation guarantee; session-peer continues to report Codex targetGeneration as unsupported. #180 remains open for a separately validated native contract.

--message TEXT (short form -m) names the text sent to the destination. --output-format text|json selects the command result format, not the message format. It is available on list, send, doctor, and update; the default is text. The existing --json is retained as an alias for --output-format json.

session-peer send --to worker --message "Report progress" --output-format json
session-peer send --to worker -m - --output-format json < message.txt
session-peer list --output-format json

Legacy positional messages and omitted-message stdin input continue to work. Use either a positional message or --message, not both. --message - reads stdin; an explicit empty message is still rejected. To send text beginning with a dash, use --message='--literal text' or stdin. Internal SSH --b64 input cannot be combined with either public message form.

--json --output-format json is valid; combining --json with --output-format text is an error in either order. Invalid/contradictory output options are argparse usage errors (stderr, exit 2); message-source conflicts are ordinary command errors (JSON when requested, exit 1). No messages are submitted in either case. JSON results still describe submission rather than receipt; these flags introduce no structured JSON message-input protocol.

Local route presets (v1.3.0)

Available in v1.3.0. The opt-in preset commands remember routes, not recipients. Use --preset to select one saved local or SSH route for ordinary Claude Code or Codex listing/sending on Mac/Linux. Windows preset operations are refused; ordinary Windows commands remain unchanged.

session-peer preset add work-mac --host user@work-mac --agent codex --codex-home /Users/user/.codex
session-peer preset list
session-peer preset show work-mac --json
session-peer list --preset work-mac
session-peer send --preset work-mac --to 'codex:<full-thread-uuid>' --message "Review the patch" --dry-run
session-peer send --preset work-mac --preset-revision 'dp1:<HASH>' --to 'codex:<full-thread-uuid>' --message "Review the patch"
session-peer preset replace work-mac --expect-revision 'dp1:<HASH>' --host user@new-mac --agent codex --codex-home /Users/user/.codex
session-peer preset remove work-mac --expect-revision 'dp1:<CURRENT-HASH>'

Replace the UUID with a current discovery result and the hash with the exact revision printed by the selected preset. A real send requires --preset-revision; a dry run may omit it. The dp1: hash covers the preset name and exact validated route values, not DNS, SSH configuration, host keys, native ownership, permission, generation or ACK. Changed route values require a fresh review; restoring identical values yields the same hash. Replacement/removal require --expect-revision, and adding an existing name is refused.

Recipient names, PIDs, UUIDs and generation tokens are never saved. --to remains mandatory; a name is freshly resolved by existing rules, not bound to an earlier session. Existing explicit --target-generation, writer/storage checks and unknown/no-automatic-resend behavior are retained. A preset label does not authenticate a person or prove delivery.

Storage is $XDG_CONFIG_HOME/session-peer/destinations.json, or ~/.config/session-peer/destinations.json when the variable is unset; the configured base must be absolute. The application directory is owner-only 0700 and files are owner-only 0600 or stricter. Symlinks, unsafe ownership/permissions, duplicate/unknown JSON keys, malformed UTF-8, more than 128 entries or more than 64 KiB are refused without falling back to another route. Names match [a-z][a-z0-9_-]{0,63}. The schema stores only transport/agent, one SSH host when needed, and an absolute Codex home for Codex routes:

{"schemaVersion":1,"presets":{"work-mac":{"transport":"ssh","agent":"codex","host":"user@work-mac","codexHome":"/Users/user/.codex"}}}

Combining a preset with explicit --host, --codex-home, --agent, --ssh-opt or --codex-bin is an error even if the values match; Reply-To URIs, paired devices, Antigravity, wake and experimental handoff combinations are also excluded. No credentials, executable paths, SSH commands/options, approval settings or receipt secrets are stored. Existing operator-owned SSH configuration still applies.

preset list and preset show read configuration only: no native discovery, SSH, update notices or background refresh; missing storage is not initialized by a read. By contrast, list --preset explicitly runs the ordinary discovery for that one route, including any existing remote probes and partial-failure behavior. No other presets are contacted and no group broadcast is added.

Text list/send operations print the chosen route and revision to stderr before the existing action. Existing native result JSON, stdout and exit contracts are unchanged; preset flags, revisions and local file paths are not forwarded over SSH. Each invocation uses one read snapshot. Later replacement/removal does not cancel an already expanded invocation; actual submission still revalidates the native target. This is not atomic storage/owner attestation, receiver deduplication, consumption, ACK or task completion.

A preset_commit_unconfirmed error means an atomic replacement may already be visible but its durability could not be confirmed. Inspect the current configuration before retrying; no automatic rollback is attempted.

Checked ancestors must be owned by root or the current user and not writable by others, except a root-owned sticky temporary directory; the immediate configuration parent must belong to the current user. All path components are opened without following symlinks. Extended ACL entries on private storage are conservatively refused; macOS ancestors may contain deny-only ACLs, but allow entries or unverifiable ACLs are refused. Linux POSIX ACLs are also checked. An empty or relative XDG_CONFIG_HOME is refused rather than silently selecting another path. Existing permissions are not repaired automatically: choose a safe, private absolute configuration root, or inspect and repair malformed configuration offline after keeping a private backup. Do not publish its contents.

On macOS, mounts that ignore ownership are refused with preset_ownership_unenforced; an unavailable or unverifiable mount query is refused with preset_mount_unverifiable. Mode bits alone are not treated as proof that ownership is enforced. On Linux, ACLs on ancestors are also refused, as are filesystems whose ACL state cannot be verified. After an interrupted management command, inspect the current configuration because the change may already be visible; directory-sync failures are not silently accepted.

v1.4.0 source: MCP presets (#359)

Input signatures: list_sessions(destination="local", agent=null, include_inactive=false, preset=null, preset_revision=null); send_message(destination, target, message, dry_run=false, wake=false, wake_timeout=30, preset=null, preset_revision=null); preflight_send(destination, target, message, wake=false, wake_timeout=30, preset=null, preset_revision=null).

MCP preset lookup and selection use an explicit operator allowlist and the existing secure snapshot reader; they add no CLI preset-management commands or automatic selection. See MCP preset policy and limits. MCP selection expands an approved route instead of combining CLI --preset with explicit routing flags. Existing CLI output, revision and filesystem-security contracts remain unchanged.

For MCP list_sessions, preflight_send and send_message, optional preset and preset_revision inputs require both the existing action permission and presets. Actual sends require the current dp1: revision and an explicit target. Wake and Reply-To URI combinations are rejected. A revision is checked during lookup, not as a target generation or authority, and later changes cannot cancel a route already expanded for invocation.

The new 64 KiB tool-result budget applies only to the list_presets metadata query. A preset is only a route selector for existing actions, with the same action authorization and exact policy-route matching. Successful list_sessions and send_message responses keep the existing CLI schema and native payload, without preset metadata, a new command or an added data projection. Existing native evidence and transport limits remain intact; real sends gain no aggregate 64 KiB limit. Inspect revisions separately with list_presets. Selected preflight_send may include only safe preset metadata: name, agent, transport and revision.

If any approved item is missing or its route mismatches policy, the whole metadata lookup is refused atomically. Fixed selector refusal codes include preset_platform_unsupported, preset_route_unsupported, preset_revision_mismatch and preset_storage_unavailable; no management or filesystem repair is attempted. Metadata-query failures precede invocation. After invocation, I/O or actual-send decode failure may follow submission: report outcome_unknown with retryAllowed=false and never retry automatically. Preserve any known native queued/submitted evidence.

Recipient addressbook (v1.3.0)

Available in v1.3.0. A route preset still stores only a route. The separate, opt-in addressbook binds a user-supplied label to an explicitly selected Claude Code or Codex recipient on a local or SSH Mac/Linux route. Labels do not authenticate people. Paired devices, Antigravity and Windows are refused. Commands without an explicit addressbook operation or --address do not read addressbook configuration; no automatic migration or fallback is added.

session-peer address resolve --preset work-mac --to 'codex:<full-thread-uuid>' --json
session-peer address add review --label Review --preset work-mac --to 'codex:<full-thread-uuid>' --expect-selection 'as1:<HASH>'
session-peer address discover --address review --json
session-peer send --address review --dry-run --message 'Review the patch'
session-peer send --address review --address-revision 'ab1:<HASH>' --confirm-target 'codex:<full-thread-uuid>' --message 'Review the patch'
session-peer address reselect review --expect-revision 'ab1:<OLD_HASH>' --to 'codex:<new-full-thread-uuid>' --expect-selection 'as1:<NEW_HASH>'
session-peer address remove review --expect-revision 'ab1:<HASH>'

Replace placeholders with the exact reported values. Before add, inspect address resolve and use its as1: observation revision. Before reselect, resolve the intended new target with the same preset again and review its new as1: value. as1: is a revision of observed selection metadata, not authority, a native epoch or proof of consumption. Every successful add or reselect creates a new bindingId, covered by the ab1: address revision. Removing and recreating an identical address therefore cannot reuse the old revision. A route's dp1: remains a value hash: restoring identical preset values gives the same hash, not a new epoch.

address list and address show read configuration only. address resolve and address discover explicitly inspect selected recipient metadata without a placeholder message, native CLI/version probe or model/session activation; existing OS process-metadata inspection may run. Discover accepts at most 16 distinct --address names, grouping only identical route/home and pinned SSH-key contexts so each group is inspected once. It retains per-address partial failures and returns exit 1 if any selection fails or no longer matches. It neither scans the network nor broadcasts to a group. A missing referenced preset or changed route/revision is refused; the stored old route is never used as a fallback.

Claude addresses retain the selected tg1: generation and use the existing connected-peer precondition before submission. Codex addresses retain a full thread UUID, exact home and aw1: writer observation, with existing checks repeated before the effect. Codex generation remains unsupported: writer observation and revalidation are not an atomic native incarnation guarantee. A changed or unavailable writer requires explicit reselection; no automatic retargeting or replay is attempted. SSH selection requires a host key verified on the actual connection and pins that same key for the actual send. This authenticates neither a person nor the identity of SSH configuration or SQLite storage.

An actual send requires both the current --address-revision and exact --confirm-target; dry-run may omit them. --address cannot be combined with --to, --preset, explicit host/native/SSH overrides, paired-device, wake, handoff or Reply-To URI routing. Normal message inputs and limits remain unchanged. Unknown outcomes never authorize automatic resend. Use send --address NAME --dry-run --explain with a normal message input for a text-only delivery preview. --explain rejects JSON output; the --address wake and handoff restrictions still apply.

Storage is a separate addresses.json beside destinations.json, with schemaVersion: 1, a 64 KiB limit and at most 128 entries. It uses the same owner, permissions, ACL, mount and no-symlink-following checks as presets, without automatically repairing or migrating files. Address names follow the preset-name syntax; display labels are normalized to NFC and limited to 128 characters, rejecting control, bidirectional-control and format characters. An unconfirmed management commit may already be visible: inspect current configuration before retrying. Binding revisions do not add receiver deduplication, ACK or task-completion evidence.

Optional interactive shorthand

For releases containing these assets, Bash/zsh and PowerShell users can opt in to a shell-local alias named sp. It invokes the currently selected session-peer command with the same arguments, stdin, output, exit codes and permissions. It is not a separate parser or a Python/TypeScript selector. Keep the canonical command in scripts, SSH requests and generated reply instructions. Do not install both implementations simultaneously; switch with the original installation manager and verify which command your shell resolves first.

The wheel and source distribution include passive activation files in session_peer_shorthand; installation does not activate them or edit profiles or PATH. Use the Python interpreter belonging to the selected installation to locate a file: an activated pip virtual environment's interpreter, or the interpreter inside the tool environment reported by pipx environment --value PIPX_LOCAL_VENVS or uv tool dir. On Unix those tool environments normally contain session-peer/bin/python; Windows uses session-peer/Scripts/python.exe. Inspect the actual environment instead of assuming its location. The module only prints the asset's path; read the file before sourcing it.

asset=$("/path/to/tool/python" -I -m session_peer_shorthand bash)
source "$asset"
sp --version
source "${asset%/*}/sp-remove.sh"
$asset = & 'C:\path\to\tool\Scripts\python.exe' -I -m session_peer_shorthand powershell
. $asset
sp --version
. $asset -Remove

Activation refuses an existing sp executable, alias or function without replacing it. PowerShell normally already defines sp as Set-ItemProperty, so activation refuses by default; continue using the canonical command unless you deliberately manage that collision yourself. Bash/zsh must source the file, and PowerShell must dot-source it. These assets do not support cmd.exe. Shell aliases require the shell's normal alias expansion; Bash disables it in non-interactive scripts, which should use the canonical command instead.

Repeated activation of this unchanged owned alias succeeds without further changes. Removal affects only the alias created in the same shell; it refuses if you replaced it with another alias or command. Closing the shell removes the alias. An upgrade through the original manager keeps the alias pointing at the canonical command, not a copied runtime. Remove the alias before uninstalling or switching implementations; uninstall does not modify a running shell. Do not add these files to profiles unless you intentionally take responsibility for persistent setup and removal.

Standalone installation remains unchanged and does not install these files automatically. To use the shorthand with standalone, obtain the files from a reviewed checkout or extract session_peer_shorthand from the corresponding verified release source distribution. Follow release verification rather than sourcing an unverified download. Source the reviewed local file directly, keeping the canonical command on PATH. Updating or removing this manually selected file is your responsibility; it does not change standalone, pipx, uv or pip ownership. Alias collision detection covers only commands visible to the current shell and PATH, not other shells or future PATH changes.

Install

Python 3.9+, standard library only — no external dependencies.

pip

In an activated virtual environment:

python -m pip install session-peer

Or with pipx for an isolated install:

pipx install session-peer

Alternatively, use uv tool install session-peer. Package managers install the session-peer command but not the agent skill. Install the skill for Claude Code, Codex, and Antigravity from its dedicated repository:

npx -y skills@latest add abruption/session-peer-skill \
  --skill session-peer --global \
  --agent claude-code --agent codex --agent antigravity \
  --copy --yes

install.sh

Verified standalone installation needs a recent GitHub CLI with attestation verification. Until a verified immutable release is published, use pipx/uv/pip; historical v1.0.2 has no verified standalone assets. Authenticate the installer before executing downloaded code using the bootstrap procedure. The commands below assume an already trusted installer checkout; its default still downloads the verified latest release. Local files require explicit --local-source; unverified development main requires explicit --main.

Installs both the command and the skill in one step. Use this for air-gapped hosts or remote deployment over SSH:

git clone https://github.com/abruption/session-peer && cd session-peer

./install.sh                          # this machine
./install.sh --host build-server      # a remote machine, over SSH
./install.sh --host web-01 --host db  # several at once

That places session_peer.py in ~/.local/share/session-peer/, installs a compatibility copy of the session-peer skill in both ~/.claude/skills/session-peer/ and ~/.agents/skills/session-peer/, and links ~/.local/bin/session-peer. Skills already installed by another manager, including symlinks, and existing cc-peer files are preserved. Remove installer-owned files with ./install.sh --uninstall [--host ...]; unmarked legacy skills remain.

session-peer update refreshes a standalone program from the latest GitHub release. ./install.sh --host <host> verifies the latest release and pushes its program and skill over SSH. session-peer update --host <host> pushes only the program when the installed version differs or is absent; add --check to report without changing anything. See Updating for package-managed installs and remote limitations.

Remote installs push the files over the SSH connection itself, so the target needs no internet access. Installing the standalone files requires python3 and SSH access. Messaging also requires the selected agent's native inbox or queue on the destination.

For explicitly trusted local development source:

# Explicitly trust reviewed local source for development only.
./install.sh --local-source

The skill

The published skill is maintained in the session-peer-skill repository. The standalone installer retains a compatibility copy for air-gapped and SSH installations and places it in ~/.claude/skills/session-peer/SKILL.md for Claude Code and ~/.agents/skills/session-peer/SKILL.md for Codex. Claude placement respects CLAUDE_CONFIG_DIR, then ANTHROPIC_CONFIG_DIR, before the default. It does not overwrite separately managed skills or remove them on uninstall. For normal global installation across Claude Code, Codex, and Antigravity, use the Skills CLI command above. The skill guides target and message selection while the Python program performs discovery and transport. Installing it does not change an agent's permissions or inbound settings.

Usage

session-peer list                                  # Claude + Codex on this machine
session-peer list --host web-01                    # Claude + Codex over there
session-peer list --host web-01 --all              # Claude stale records / no inbox
session-peer list --agent codex --all              # include archived Codex threads
session-peer doctor                                # local inbox/tool/home diagnostics
session-peer doctor --host web-01                  # run the same checks there
session-peer doctor --host web-01 --check-return-route  # also test SSH back here

session-peer send --to api-worker "message"        # local session
session-peer send --host web-01 --to api-worker "message"
session-peer send --host deploy@web-01 --to api-worker "message"  # explicit SSH user
session-peer send --host web-01 --to 4011 "message"          # address by pid
git log --oneline -5 | session-peer send --host web-01 --to api-worker -   # stdin

session-peer send --host web-01 --to api-worker --dry-run "x"   # resolve only
session-peer list --host web-01 --json             # machine-readable
session-peer list --no-update-notice                # disable cached update notices/checks

session-peer send --host web-01 --ssh-opt=-p --ssh-opt=2222 --to api-worker "..."   # note the '='

# Envelope. Sends identify the Claude/Codex sender and how to answer when the
# current agent session and a return route can be detected.
session-peer send --host web-01 --to api-worker --no-reply-to "..."         # no return address
session-peer send --host web-01 --to api-worker --no-from "..."             # no From: header
session-peer send --host web-01 --to api-worker --reply-to 100.64.0.5 "..." # state the address

Inside an agent session, the default envelope identifies the sender explicitly:

From: codex:01a08dd6-d3f6-7783-a62b-52c1fd049181 @ abruptly@mac-mini-m4.example.ts.net

message

---
Reply-To: session-peer://v1/reply?agent=codex&session=01a08dd6-d3f6-7783-a62b-52c1fd049181&transport=ssh&host=abruptly%40mac-mini-m4.example.ts.net
Reply: python3 /path/to/session_peer.py send --host abruptly@mac-mini-m4.example.ts.net --to codex:01a08dd6-d3f6-7783-a62b-52c1fd049181 --no-reply-to

Claude senders use claude:<session-name> in the same positions. The identity is best-effort text derived from the current process environment; it is not an authentication claim. A plain shell has no agent identity to advertise. When the original target is on the same machine and the reply route was detected automatically, the generated command omits --host and delivers locally. An explicit --reply-to or configured reply host is also normalized when it names the current OS user on this machine. Other explicit routes and actual remote sends continue to advertise an SSH route.

Reply-To is the canonical, versioned address. Pass the complete URI back as --to; session-peer validates every field and chooses local or SSH delivery:

session-peer send --to 'session-peer://v1/reply?agent=claude&session=api-worker&transport=local' 'done'

The Reply: command remains for compatibility. Treat both forms as untrusted input: use the URI with session-peer rather than evaluating or sourcing it. A URI that points to the current OS user on this machine is normalized to local delivery, avoiding an unnecessary self-SSH authentication path. Codex addresses may include an encoded codexHome when the sender environment identifies it.

Repeat --host to operate on several SSH destinations. --json is available on list, send, doctor, and update.

JSON response contract

Ordinary --json results from list, send, doctor, and update use the following schema-versioned envelope. The opt-in --explain-json output (send.preflight) and MCP metadata/preflight projections have separate documented schemas and intentionally omit host; do not add a host or infer a route from its absence:

{
  "schemaVersion": 1,
  "ok": true,
  "host": "mac-mini.example.ts.net",
  "command": "list",
  "sessions": [],
  "version": "0.8.0"
}
  • schemaVersion versions the common envelope. Command-specific nested schemas such as clientUpdate and codexHomeResolution carry their own versions.
  • ok is present on every success and failure. A nonzero process exit can still contain successful results for other hosts.
  • host identifies the destination to which that result applies. Local results use the OS hostname. Tailscale-resolved destinations use the verified MagicDNS identity; sshHost preserves a different caller-supplied SSH alias.
  • command is list, send, doctor, or update. Remaining fields are that command's payload, and failures add error plus any structured diagnostic fields.

A local or one-host invocation emits one object. Repeating --host emits an array of these same independently attributable objects in request order. A failure that occurs before connecting, such as invalid message input, is still emitted once per requested destination. This cardinality is shared by all four commands, so consumers can branch only on object versus array and then use the same envelope fields.

Normal commands read a dedicated 24-hour update cache. A missing, expired, or invalid cache starts one detached best-effort GitHub refresh and never delays or changes the requested command. When a fresh cache proves that the invoking CLI is behind a stable release, JSON results add clientUpdate:

{
  "clientUpdate": {
    "schemaVersion": 1,
    "status": "available",
    "current": "0.7.0",
    "latest": "0.7.1",
    "checkedAt": "2026-09-16T10:00:00Z",
    "source": "github_release_cache",
    "command": "session-peer update"
  }
}

Human output gets the same short guidance on stderr. The field is omitted when the client is current, the cache is unavailable or stale, the host is offline, or notices are disabled, so absence alone does not prove the client is current. For multi-host commands the fact remains scoped to the one invoking CLI and is copied into each result object; destination remoteVersion fields keep their separate meaning. Remote subprocesses do not perform their own refresh.

When local tailscale status --json identifies a --host by device hostname, short MagicDNS name, full MagicDNS name, or Tailscale IP, session-peer verifies and reports the current MagicDNS FQDN as host. SSH still receives the supplied value as its destination alias, reported separately as sshHost when different, while a HostName override routes the connection to that FQDN and HostKeyAlias retains the existing host-key lookup. This preserves matching Host, User, Port, and IdentityFile settings. A known peer reported offline fails before SSH. Hosts absent from the tailnet map remain normal SSH destinations.

Host identity does not supply a login account: known_hosts, Tailscale peers, and MagicDNS names identify a machine, not its OS users. Specify the account as --host USER@HOST, or configure it for the original alias:

Host web-01
    User deploy

An explicit USER@HOST takes precedence. Otherwise session-peer runs ssh -G with the same alias and options to report the effective OpenSSH configuration or local-user default. It never guesses from another machine or retries a failed login under different usernames.

Successful remote results and SSH connection failures add sshUser and sshUserSource; the source is explicit, ssh_config_or_local_default, or unknown when ssh -G cannot resolve it. Connection failures also add sshFailure, classified as authentication_failed, host_key_failed, timeout, or transport_failed. Authentication errors direct the caller to --host USER@HOST or the original alias's SSH User setting and are never retried.

Exit codes: 0 successful command (including listing or dry-run), 1 operational error, 2 CLI usage error or an unresolved target reported as no-target, and 130 interrupted (Ctrl-C). A Codex queue rejection, including a missing rollout, is an operational error (1); a missing saved thread during dry-run returns 2. Exit 0 on send does not confirm consumption or a reply.

Bounded handoff watch (v1.3.0)

session-peer handoff status --correlation-id PREPARED_UUID --json
session-peer handoff watch --correlation-id PREPARED_UUID --watch-timeout 30 --interval-ms 1000 --json

Use an existing prepared UUID in the local owner's POSIX ledger. Status remains one snapshot; watch polls for 1..60 seconds (default 30) at 1000..5000 millisecond intervals (default 1000), accepting ASCII integers only and taking at most 61 samples. Watch emits an initial snapshot only if the budget permits a valid public projection, then changed public metadata/historyPhase only; it may emit no snapshots. JSON watch output uses the common schemaVersion 1 NDJSON envelope with operation watch and event snapshot or end, nesting the unchanged handoff object in snapshots; status JSON is unchanged. Timeout completion is exit 0 for watch completion only, not ACK or wait success; SIGINT is 130, history/output failure 1, invalid arguments 2. Missing/corrupt history or ledgerEpoch change stops watching. historyPhase is prepared, attempted, terminal, tombstone or quarantined; attempted means a durable attempt fence, not active execution.

Watch is read-only: no intents, waits, receipts, ledger writes, submission or wake. It adds no host/device/Windows route or Codex ACK. Limits are 16 KiB per frame and 256 KiB total with a 2 KiB end reserve. Real-fd stdout uses one owned isolated output-only Python helper that sees only bounded public serialized bytes, never changes shared descriptor flags and reads no ledger/native data. The parent enforces the original total budget including IPC, checks it before new ledger read/validation stages, and reserves 100 ms within it for final output and cleanup, so sampling may end earlier. It terminates and reaps its helper on timeout, SIGINT or error; uninterruptible OS operations prevent a hard-real-time guarantee. Partial or unacknowledged writes prohibit subsequent/end frames; consumers must reject truncated final NDJSON lines. Helper completion means OS write completion, not recipient consumption. Snapshots are not a lossless event stream, and local UTC observations do not prove ACK arrival order. See the handoff runtime guide for privacy, retained submission/ACK/wait meanings, read-only collector queries and the existing quarantine gate. Watch launches no native message process or collector. No payload/capability digests are exposed; public targetGeneration keeps its existing contract. The read-only ledger releases its shared lock after reading immutable snapshot bytes, before parsing and validation. Interrupted final output and cleanup are capped at 100 ms within the remaining total budget. Overall success requires a complete successful end frame and exit 0: cleanup may fail after that frame. A nonzero exit means overall failure, without promoting ACK, discarding independently valid submission facts or permitting retry.

Trusted local SSH configuration (v1.3.0)

On Linux and macOS, an explicit local CLI invocation of list, send, doctor, or update with --host can select an existing OpenSSH configuration by pairing --ssh-config ABSOLUTE_PATH with --trust-ssh-config. Both flags are required. For example, using a placeholder path (quoted spaces are supported):

session-peer list --host worker --ssh-config '/home/alice/SSH Profiles/work.conf' --trust-ssh-config
session-peer doctor --host worker --ssh-config '/home/alice/SSH Profiles/work.conf' --trust-ssh-config --check-return-route

This grants local code-execution authority to the selected configuration: Match exec can run even during ssh -G identity discovery or send --dry-run. The operator must also trust Include files, ProxyCommand, and other external dependencies. Validation is not a configuration sandbox and does not recursively validate those dependencies. No existing profile or configuration file is edited.

The path must be absolute, with no .. components, control characters, or symlinks in any component. The selected file must be a regular file owned by the current effective user, have exactly one hard link, be at most 64 KiB, and have neither group/other write permissions nor setuid/setgid/sticky bits. Ancestor directories must be owned by root or the current effective user and must not be group/other writable, except for root-owned sticky directories. Linux rejects extended access ACLs and directory default ACLs; macOS rejects allow ACL entries and volumes with ownership disabled. ACL or ownership checks that cannot be verified refuse the selection. These are conservative checks, not a full effective-permissions interpreter.

One selection is reused throughout that CLI call for local ssh -G discovery, doctor, remote listing/version checks, update probes/install/verification, and send/handoff SSH calls. Before each SSH spawn it revalidates file identity, metadata, content hash, and storage safety. Detected changes refuse the operation without falling back to default configuration. OpenSSH opens the original path; this is not an immutable byte snapshot and a same-user change between validation and OpenSSH's read remains possible.

This mode forces StrictHostKeyChecking=yes, UpdateHostKeys=no, ForwardAgent=no, PermitLocalCommand=no, and BatchMode=yes, and disables connection multiplexing. Existing explicit host-key pins retain priority. The --ssh-opt allowlist is unchanged: -F and ProxyCommand remain forbidden there. The selected local path is not forwarded to the remote command, reply route, or public JSON. Reverse doctor checks use the remote machine's own configuration; they never inherit this path. Forward success does not prove reverse authentication, message consumption, or an ACK.

Environment variables, MCP requests, ReplyTo addresses, and stored presets cannot select this mode. Windows, setup, paired-device routes, preset combinations, reply-URI destinations, and invocations without --host are rejected before reading the configuration or starting SSH. Omitting the pair preserves the existing default SSH behavior.

One-time pairing links (v1.3.0)

Both endpoints need pairing-offer-v1 and the existing Unix Python 3.11+ relay extra. The following commands are available in v1.3.0:

session-peer device rotation-status --state /private/client-state
session-peer device offer --state /private/device-state --peer-key CLIENT-CURRENT-64-HEX-KEY --direct PRIVATE-IP:3770 --show-link
session-peer device pair --state /private/client-state --link --route direct
session-peer device offer-status --state /private/device-state --offer-id OFFER-UUID
session-peer device offer-cancel --state /private/device-state --offer-id OFFER-UUID

Creation confirms both identities and shows the link only on the owner terminal. --link takes no value. Link input is hidden and bounded. The client asks the operator to enter the receiver's current fingerprint obtained independently through a trusted channel and requires a match before trust staging or networking. No terminal means refusal, not echo fallback or automatic consent. Never put a link in argv, browser history, logs or issue reports. Guided setup offers link/private-file choices; existing file commands remain supported. Cancellation affects only that offer's pending trust, not completed pairing; use explicit peer revocation after completion. Expired cold links are refused; only exact saved context can reconcile an already-consumed authenticated result. Pairing never replaces the receiving target policy or native approval. See the paired-device guide for key-rotation restrictions, expiry, quotas, interruption and the threat model.

Updating

On macOS/Linux, hide the automatic runtime notice for one explicitly selected stable version, or clear that preference:

session-peer update --dismiss-runtime-notice 1.3.1
session-peer update --clear-runtime-notice-dismissal

The version above is an example, not a release announcement. These local-only, offline commands do not check, install or approve a release. They cannot be combined with remote/SSH options or the check option. Only the exact normalized version is hidden; later releases can still produce a notice, and clearing restores the usual behavior. Explicit local checks retain the real latest version and outdated result, with separate preference metadata. Remote checks, skill notices and the existing global opt-out are unchanged. A remote command may still show the calling CLI's own cached notice; its local preference is read once, not once per target. Remote versions and results are never hidden by that preference.

The preference is stored separately from the update cache at $XDG_CONFIG_HOME/session-peer/update-notices.json, or ~/.config/session-peer/update-notices.json when that variable is unset or empty. The directory and file must be private and safely owned; unsafe paths are refused, not repaired or redirected. Invalid or future-schema files hide nothing and are not overwritten by either command: inspect the dedicated preference file first. Windows does not yet support this per-version preference; ordinary updates, checks and the global opt-out remain available.

Safety checks cover POSIX owner/mode bits, not a full ACL audit or physical-media durability certification. Symlinked ancestors and explicitly relative config paths are intentionally refused without fallback. The filesystem must support directory-relative I/O, file locking and directory fsync. After inspecting and backing up a malformed preference, an operator may explicitly remove only that dedicated JSON file and run the preference command again; do not remove the lock file while writers may be running. No automatic repair is performed.

For package-managed installs, use the same manager that installed the command:

pipx upgrade session-peer
# or: uv tool upgrade session-peer
# or, in its virtual environment: python -m pip install --upgrade session-peer

For these installs, local session-peer update prints package-manager guidance without replacing package-owned files. session-peer update --check checks the latest stable GitHub release, refreshes the shared cache, and reports the exact upgrade command, but still does not replace those files.

For standalone programs:

session-peer update --check                       # check the latest GitHub release
session-peer update                               # update the local program
session-peer update --host web-01 --check         # inspect the remote standalone copy
session-peer update --host web-01                 # push this program if versions differ

Remote update compares against the local program's version, not the latest GitHub release. It pushes only if the remote copy is missing or older, and leaves a newer remote version unchanged. Check first and update the local program before distributing it. The remote copy does not fetch from GitHub. Remote version probes inspect only ~/.local/share/session-peer/session_peer.py, not pip/pipx/uv installations; remote update installs that standalone path and its CLI link. For a package-managed remote CLI, upgrade it with its own manager on that host instead.

Neither local nor remote update refreshes the Claude skill. Re-run install.sh from the desired release checkout to refresh both standalone program and skill. Local session-peer update --check and session-peer update also populate the same cache used by automatic notices.

Claude profiles managed by Orca

On macOS, when neither CLAUDE_CONFIG_DIR nor ANTHROPIC_CONFIG_DIR selects a directory, discovery includes ~/.claude/sessions and known same-user Orca ~/Library/Application Support/orca/claude-profiles/*/home/sessions directories. An explicit environment directory retains its original scope and precedence; it never expands into other profiles. For example, use CLAUDE_CONFIG_DIR="/path/to/profile/home" session-peer list --agent claude --json to inspect one profile only. Installation paths are unchanged.

On all platforms, default and explicit registry scans share a total limit of 4,096 directory entries and bounded 1 MiB registration files. Only the additional Orca profile inventory is macOS-specific, limited to 128 immediate entries. Automatic profile directories reject symlinks, reparse points and foreign-user ownership. Discovery and doctor never read credentials, inbox keys or transcripts. Canonical aliases of one registry are scanned once. Distinct records claiming the same live PID or inbox conflict even when their public metadata is identical.

Partial errors preserve healthy listing rows, but any inventory error, including a live conflict, refuses every target selection, even an unrelated healthy PID, for dry-run, send and handoff. An explicit directory can isolate errors in other profiles; it cannot repair conflicts or limits within the selected directory. Legacy single-directory handling of malformed records is unchanged. Selected record and endpoint evidence is rechecked without rescanning other profiles. Ordinary Unix socket aliases remain supported; their existing optional generation limitation remains. A generation token is not send permission or ACK. These checks do not authenticate a model, remove all filesystem races or prove receipt. Other Orca layouts and arbitrary configuration roots are not scanned.

Environment variables

Variable Effect
SESSION_PEER_REPLY_HOST Override the advertised reply host: --reply-to → SESSION_PEER_REPLY_HOST → CC_PEER_REPLY_HOST → auto-detected Tailscale MagicDNS name or IP. A detectable Claude or Codex sender session is still required for a reply line.
CC_PEER_REPLY_HOST Legacy fallback; prefer SESSION_PEER_REPLY_HOST for new configuration.
CLAUDE_CONFIG_DIR Where Claude Code keeps its config (default ~/.claude). Respected by session-peer list for session discovery and by install.sh for skill placement.
ANTHROPIC_CONFIG_DIR Fallback if CLAUDE_CONFIG_DIR is unset.
CODEX_HOME Codex discovery/queue home (default ~/.codex); overridden by --codex-home.
SESSION_PEER_CODEX_HOMES Additional destination homes to check for duplicate thread UUIDs and stable live writers before an implicit send/dry-run. JSON array of absolute paths (or ~/…), not a shell command or a path-separated list. It does not merge listings; an unambiguous active writer may change the implicit send home.
SESSION_PEER_NO_UPDATE_NOTICE Set to 1, true, yes, or on to disable automatic cached update notices and background refreshes. The per-command equivalent is --no-update-notice. Explicit session-peer update --check still checks.
XDG_CACHE_HOME Base directory for the update cache; otherwise ~/.cache/session-peer/update.json is used.

With --host, discovery uses the destination's environment; local environment variables are not automatically forwarded. --codex-home and --codex-bin explicitly select paths on that destination.

Codex sessions

Codex discovery reads state_5.sqlite using a read-only SQLite connection. This internal schema is experimental, tested with Codex CLI 0.154.0 on macOS; cross-platform fixture tests are not a claim of live Codex verification on all OSes. Saved sessions are not necessarily active. --all includes archived threads.

--codex-home overrides the destination's CODEX_HOME (default ~/.codex). --codex-bin overrides its PATH lookup of codex for sending. On SSH these are remote paths. Sending requires a Codex executable with the queue command and the appropriate saved thread/rollout in that home; a listing alone does not prove the queue can accept it.

SQLite storage boundary

Standalone list remains metadata-only: it does not execute Codex or query its version, and --codex-bin does not establish the version for discovery. On macOS, discovery therefore conservatively checks ordinary as well as forced preferences before opening a candidate SQLite database. This blocks stale-home results for older loaders, but can also refuse a valid newer environment whose native loader ignores ordinary values. A refusal is an error, never a successful empty list or a switch to another database; valid results from other agents remain available with the incomplete-discovery error. The diagnostic identifies the storage-configuration ambiguity and asks the operator to review known configuration layers, including effective com.openai.codex configuration/requirements and the intended home/storage layout. Changing --codex-home alone does not resolve global preference ambiguity. Do not automatically remove preferences, managed profiles or databases to suppress the error. If the native loader legitimately ignores the value, discovery remains conservatively unsupported for that configuration; precise version-aware discovery is a separate improvement. Internal send dry-run discovery reuses the already selected send policy without another probe.

For sends and their internal dry-run checks, the macOS preference policy depends on the selected native executable. The exact reviewed Codex 0.162.1 loader accepts only forced values; Codex 0.154.0 reads ordinary as well as forced preferences through CFPreferencesCopyAppValue. On macOS, ordinary queue sends and dry-runs run one bounded --version child in the selected home and working directory: three seconds for the query and two for cleanup, with 1 MiB stdout and 64 KiB diagnostic limits. Output is never reflected; malformed, unavailable or unreviewed versions conservatively include ordinary preferences rather than refusing solely for their version. Uncertain process cleanup refuses before queue submission, and interruption is propagated after the bounded cleanup attempt. Only the exact reviewed codex-cli 0.162.1 selects forced-only checks; later versions are not assumed equivalent. Handoff reuses its existing version query and original deadline, then retains the selected policy through submission. The supported 0.154.0 wake path includes ordinary preferences even when an active writer only receives a queue submission. Storage and custom-marker checks repeat before the budget and child boundaries. No new version probe is added outside macOS, no results are cached, and wake support is not expanded. A refusal after known queue submission preserves that submission without resending. These are source-based guards and synthetic regressions, not new native preference or MDM validation.

Owned queue and wake children use an existing, strictly resolved physical working directory, including aliases in any ancestor. Project scanning, child cwd, POSIX PWD and wake thread/resume.cwd use that directory. For a saved consumer directory that still exists, both physical and distinct lexical project spellings are checked with the same marker boundaries and custom-marker fallback; this can conservatively refuse an alias configuration. A missing consumer directory still has no layers to inspect. This does not establish an already running writer's startup configuration, and alias/configuration changes after inspection remain possible. The wake.cwd result uses this physical spelling too; it can differ from the saved alias.

The native loader's layer-list comment mentions <cwd>/config.toml, but its pinned project-layer construction loads .codex/config.toml instead. This guard follows that implementation, not the stale comment. A successful Windows Known Folder lookup with an invalid or unbounded path is refused rather than silently falling back to a different directory.

Wake preflight checks known storage before reserving its budget, but a later recheck can still refuse after reservation if configuration or writer evidence changes. The translated-native-home route retains its inherited cwd and does not inspect the Windows consumer directory.

The 2026-10-09 source investigation examined native Codex 0.154.0 and 0.159.0. Both resolve SQLite storage from configured sqlite_home, then nonempty CODEX_SQLITE_HOME, then CODEX_HOME; relative environment paths use the native resolved working directory. Managed requirements can replace configured storage, including command-line overrides. The native queue path can reuse a running shared server, whose storage is not established by the new client's environment. These are source findings, not new live-delivery or platform validation.

The 2026-10-15 investigation of native Codex 0.162.1 found the storage precedence to be managed requirements, then the merged configuration layers, then CODEX_SQLITE_HOME, then CODEX_HOME. Layers that can set sqlite_home include system files, macOS managed profiles, the user's config.toml, a profile file selected with --profile, and the project layers of the native process working directory (.codex/config.toml from the working directory up to the nearest default .git root, or only the working directory when no root is found; enabled only for trusted projects). Legacy [profiles.*] tables have no storage key. codex queue submits through a running app-server daemon when one accepts a connection on the home's app-server-control/app-server-control.sock, and otherwise through an embedded server. Native TUIs start such a daemon automatically by default unless excluded, for example with --no-daemon. A daemon's QueueStore keeps the database chosen at its startup, while config/read rereads the current disk layers; initialize reports the home and platform, but the reviewed initialize, config/read and queue APIs offer no stable attestation contract for the startup SQLite identity. A matching thread UUID or the same server connection does not establish storage identity (a copied database can hold the same UUID), and the queue API has no expected-storage or incarnation precondition. A process that consumes queued messages polls the queue in its own storage. These are source findings, not live-delivery or platform validation.

session-peer refuses a nonempty inherited CODEX_SQLITE_HOME or a root sqlite_home setting in any inspected home's configuration before discovery, dry-run, or queue submission. This includes quoted/escaped root keys and root dotted/table forms, even when the configured path equals the selected home. Unselected nested keys are not root storage settings. Empty environment overrides are removed from native child environments; environment names are case-sensitive on POSIX and case-insensitive on Windows. The guard retains the selected native-home translation where already supported and does not add relocated-storage support. It also refuses a root sqlite_home in the system configuration and requirements files (/etc/codex/config.toml, /etc/codex/managed_config.toml, /etc/codex/requirements.toml, or the Known Folder API's ProgramData directory followed by OpenAI\Codex\config.toml and requirements.toml on Windows) and in macOS managed-profile values, regardless of project trust. A queue submission starts the native process in the inspected home, so the caller's directory adds no project layers. Dry-run checks the same execution plan (home, child working directory, environment, known layers and consumer directory) without queue/app-server children (wake dry-run and ordinary macOS queue dry-run may run the native version probe), and a real send repeats the checks immediately before its effect; neither removes the time gap before native Codex reads its configuration, so a passing dry-run does not guarantee storage agreement for a later send. By user policy, a daemon control socket, live or stale, is neither a refusal reason nor probed: native Codex selects the daemon or embedded server as before, and no attestation of effective storage is claimed. Outside the translated-native-home route, dry-run, queue and wake also inspect the project layers of the saved thread directory, where the consuming writer or wake server normally starts; unreadable or malformed thread metadata is refused. Wake runs these storage checks before reserving its per-target budget. Refusal reasons are sqlite_environment_override, sqlite_home_configuration, sqlite_home_system_configuration, sqlite_home_managed_configuration, sqlite_home_project_configuration, consumer_directory_unverifiable, storage_config_unreadable and storage_config_unverifiable.

Each inspected TOML file must be a regular, non-symlink UTF-8 file of at most 1 MiB. On Windows, the system directory is obtained through SHGetKnownFolderPath(FOLDERID_ProgramData), with native Codex's fixed C:\ProgramData fallback on API failure; the ProgramData environment variable is not used. On macOS, CoreFoundation synchronizes the Codex preference domain and releases owned values. The forced-only policy rechecks forced status after copying and ignores ordinary user preferences; the version-selected conservative policy also reads ordinary values, as described above. Selected base64 strings are bounded to 2 MiB and decode to UTF-8 TOML of at most 1 MiB; synchronization, type, decoding or size errors refuse. Missing TOML files or preference values are allowed. Default project markers stop at the nearest non-directory .git entry (metadata only) or a directory containing HEAD; symlink markers are conservatively refused; without a marker only cwd applies. <cwd>/config.toml is not a project layer. A known root project_root_markers override retains a conservative ancestor superset instead of claiming exact custom-marker interpretation. These APIs are covered by mocks; relocated Windows Known Folders and real forced MDM profiles have not been tested. Unreadable, changed, oversized, or lexically unclassifiable files are refused without printing their contents. The dependency-free key classifier supports Python 3.9 and is not a general TOML/schema validator; native Codex still validates other values. Queue and wake recheck this boundary before starting a child. A refusal has status=refused, submitted=false, and retryAllowed=false; valid ordinary outputs and writer-ownership checks remain unchanged. SSH performs this check on the destination, not on the sender's environment.

Translated native homes (a WSL relay receiver queueing to native Windows Codex) keep their existing submission contract, but storage verification is not supported for them: this POSIX process cannot inspect the Windows-side home spelling, case rules, environment, or system, managed and project layers. A future strict mode could refuse such paths before any effect; it is not implemented.

This is bounded hardening, not proof of effective native storage for every configuration. Not inspected: package-supplied defaults; enterprise cloud configuration bundles; the startup storage of a running shared daemon; a --profile selection, -c overrides or the environment of a writer that is already running; a writer that started outside the saved thread directory or whose directory no longer exists; and, for a translated native home, the Windows-side system, managed and project layers. Configuration can also change after the final check. Do not infer storage agreement from a successful check alone. #268 remains open for native platform validation of this contract. Fake-child fixtures show refusal and environment behavior only; they do not establish actual wrong-target delivery, a security exploit, or new native compatibility. The wake version matrix remains unchanged.

Orca and multiple Codex homes

An Orca-launched session can use a per-account home while a separate terminal or SSH command uses ~/.codex. The same UUID can exist in both. A successful queue submission to one copy does not establish that the intended session is using that home.

An explicit destination home remains the strongest selection. For example, replace <account-id> and <full-thread-uuid> with the intended account and thread:

session-peer list --host mac --agent codex \
  --codex-home '~/Library/Application Support/orca/codex-accounts/<account-id>/home' --json
session-peer send --host mac --to 'codex:<full-thread-uuid>' \
  --codex-home '~/Library/Application Support/orca/codex-accounts/<account-id>/home' \
  --dry-run --json "message"

Omit --host mac for local use. Remove --dry-run only when ready to submit. Quoting ~ keeps expansion on the destination; an absolute remote path also works. Current releases treat --codex-home as a destination constraint and still validate every matching known home's writer evidence. Older releases accept the explicit-home syntax but do not provide this safety guarantee.

The duplicate-home rejection baseline shipped in v0.6.1. The active-writer selection, revalidation, and detailed codexHomeResolution evidence described below ship in v0.6.2; v0.6.1 requires an explicit --codex-home when the same thread UUID exists in more than one known home.

Without --codex-home, selection still uses destination CODEX_HOME, then ~/.codex. Before send or dry-run, the ambiguity guard checks a bounded inventory:

  • The selected home and the default ~/.codex when its state DB exists.
  • On macOS only, existing state DBs immediately under ~/Library/Application Support/orca/codex-accounts/*/home.
  • Additional homes from destination SESSION_PEER_CODEX_HOMES, for example:
export SESSION_PEER_CODEX_HOMES='["/srv/codex/account-a", "/srv/codex/account-b"]'

Configure this in the destination command's environment; a local export is not forwarded by --host. On Windows, use absolute Windows paths with backslashes escaped as required by JSON. Empty configuration arrays are allowed; malformed configuration is an error for implicit sends.

For every send and dry-run, session-peer finds every known home that saves the UUID and examines its exact thread-writer-locks/<uuid>.lock. This includes a single candidate and an explicit --codex-home. It independently probes the kernel advisory lock and correlates the opener through two stable lsof observations. An implicit send selects the only stable, same-user Codex writer. An explicit home must be that writer; if another home owns the only live writer, the command returns a structured conflict and never changes the home silently. The selected PID, process start time, and held lock are checked again immediately before queue submission. A free or stale lock does not win merely because its file exists.

Unknown evidence, multiple live writers, missing lsof, permission failures, changing PIDs/start times/inodes, and conflicting evidence fail closed before queueing. Archived saved copies still count. Unreadable or incompatible known databases and missing configured databases also prevent submission. If every saved copy is inactive, intentionally queueing for a future resume requires an explicit --codex-home together with --allow-inactive-codex-home; --wake already serves as the explicit activation opt-in. The inactive flag without an explicit home is rejected. Resolved symlink aliases and repeated paths count as one home.

list aggregates known homes unless --codex-home selects exactly one. There is no general filesystem scan or process environment inspection, and process arguments are not exposed. Activity inspection runs on the destination machine, including over SSH. Platforms without POSIX flock or lsof cannot establish live-writer ownership and fail closed when that evidence is required. Unconfigured/custom layouts and copies created after the check can still be missed. Use session-peer doctor to inspect the selected home, bounded candidates, executable, and supported state DB schema without submitting.

Submission and JSON results

Submission uses codex queue, never direct database writes. queued means the CLI accepted the submission, not that a turn consumed it or acknowledged it. session-peer does not wake or resume sessions. Queue DB writes and Claude socket connections may require approval in the caller's execution environment; the tool does not change sandbox or inbound policies. A queue timeout (30 seconds) has an unknown submission outcome: inspect the destination before retrying. It is not a timeout for waiting on a reply, and session-peer does not automatically retry.

Codex messages are limited to 32 KiB of UTF-8 including sender/reply headers, as a session-peer portability policy rather than a measured Codex server limit. NUL characters cannot be passed as CLI arguments. --dry-run verifies the executable and saved target without queueing but cannot guarantee a later submission will succeed.

Local Codex list JSON uses the common response envelope and includes sessions, version, and per-home diagnostics in discovery.codex.homes. Each session entry has agent, id, name (first line, at most 120 characters), cwd, updatedAt (Unix seconds), archived, canonical codexHome, and stateDb. Top-level codexHome is retained only for a single candidate home without inventory errors. Each successful remote result includes the same fields and an optional remoteVersion for an installed standalone copy. One remote host returns an object; repeated hosts return an array.

The installed-version probe is supplementary: its failure preserves discovery and the primary result's exit status. Such results omit remoteVersion and include remoteVersionProbe: {status: "unknown", reason: ...} with a bounded reason (timeout, transport_failed, authentication_failed, host_key_failed, invalid_response, or probe_failed), without raw probe diagnostics. Human output shows a version-unknown notice. A confirmed uninstalled remote continues to omit both version fields. Remote update verification remains mandatory.

Within the common envelope, Codex send JSON includes target: {agent, id}, status: queued (or validated under dry-run), chars, dryRun, and optional queueId. It also includes:

  • codexHome: the resolved absolute destination home, not a sender-side guess.
  • codexHomeResolution: schema-versioned status, selected, reason, and bounded candidate evidence. Status is explicit, selected, ambiguous, or unknown; candidates expose saved-thread, writer-lock, stable owner PID and process start-time facts without process arguments or environment values.
  • submitted: true only after successful queue CLI completion, false for dry-run.
  • consumptionConfirmed: always false; neither queued nor validated establishes consumption.

Errors set the common envelope's ok to false, add error, and include codexHomeResolution when home evidence caused the failure. A timeout has an unknown submission outcome; missing submitted on an error must not be interpreted as proof that nothing was queued. Listing results do not describe a submission and have no submission/consumption fields.

A local Codex queue timeout or an SSH send without a trustworthy complete response exposes status: unknown, reason: outcome_unknown, and retryAllowed: false. SSH exit 255 or 137, missing or malformed stdout, and authentication, host-key or interpreter diagnostics alone do not prove that nothing was submitted. A complete versioned response preserves its success, refusal and native submission facts even if SSH then fails or times out; invalid UTF-8 in diagnostic stderr cannot invalidate valid protocol stdout. Pre-execution failures and completed refusals retain their existing fields. No automatic retry or fallback is attempted, and unknown outcomes never add submitted: false. Missing retry metadata is not permission to resend.

Legacy send objects without a schema remain accepted on ordinary exits 0/1/2 when ok is a boolean and any command field equals send. They cannot prove completion after an abnormal exit or shutdown timeout.

For every command, one remote host returns a flat object and multiple hosts return an array. When CODEX_THREAD_ID (or the compatibility fallback CODEX_SESSION_ID) is present, the message envelope and reply command identify the originating Codex thread.

Diagnostics and reply observation

doctor performs read-only checks on the machine that owns the sessions. It reports Claude's configured sessions directory and inbox availability, Codex's executable and bounded home/state DB candidates, unsupported DB schemas, and permission or unknown failures as distinct codes. It does not connect to an inbox, write a queue, scan arbitrary directories, or alter agent/SSH settings.

v1.4.0 source (#358): doctor --agent NAME restricts checks and declared agent capabilities to the selected registered agent, including execution over SSH. It also skips the shared installed-skill inventory. doctor --agent codex --selected-home-only --codex-home /absolute/home checks only the selected Codex home and skips the inventory of other homes. These options do not enable reverse-route checks. Without them, existing all-agent and bounded multi-home diagnostics keep their behavior.

The MCP wrapper supports only the following checks and effects:

Mode / agent Checked facts Process / connection effects
Metadata Policy and adapter declarations None
Active Claude Registry records, process identity metadata and filesystem inbox presence No inbox connection
Active Codex Executable presence, selected-home read-only SQLite queries and bounded writer scan No native Codex execution or version query
Active Antigravity Registered bridge metadata No bridge or native RPC
Active SSH Configured forward route, only after explicit probe permission SSH configuration may execute commands; no reverse probe or automatic connection back to a return host

Representative allowlisted codes include sessions_dir_missing, inbox_present, codex_executable_missing, state_db_readable, unsaved_writer_check_unavailable and adapter_failed. Results are limited to permitted agents and at most 32 checks per agent; these codes describe only the bounded scope above.

ok describes collection and format completeness, not availability. A complete missing_home result can have ok=true; read each agent's status separately. diagnosticEvidence is point_in_time_only for active results and not_collected in metadata mode. readiness remains not_checked because no per-target readiness is tested. A passing state_db_readable check means only that the read-only database query succeeded; it is not an effective-storage attestation or a return-route guarantee.

The stages tcpAccept, tls and peerAuthentication remain not_checked. Here peerAuthentication refers to paired-device authentication, separate from forward SSH authentication. sshFailure reports only a sanitized forward SSH failure category, such as authentication_failed or host_key_failed. No TCP, TLS, paired-device or return-host probe is added.

Reverse SSH is checked only with --check-return-route. The destination runs a fixed ssh ... true probe with prompts, password authentication, host-key enrollment, and config mutation disabled. Forward SSH success is never reused as proof that the reverse path works. Use --reply-to USER@HOST when automatic Tailscale detection cannot identify the origin. JSON details and status values are documented in docs/diagnostics.md.

There is deliberately no general --wait. Claude Code has a native same-machine notify_when_idle facility, but it does not cover remote sessions, subagents, or Codex, and a successful socket/queue submission is not an acknowledgement. session-peer therefore reports capabilities.replyObservation.status: unsupported instead of tailing mutable transcripts and risking a false match. Ask the target to send an explicit reply to the supplied Reply-To address when completion matters. Optional wake remains tracked in #46.

Moving from cc-peer

The repository rename and package transition are complete: session-peer 0.6.0 is published and cc-peer 0.5.1 is the final Claude-only compatibility release. The old PyPI cc-peer project is archived; the GitHub session-peer repository remains active. Existing legacy distributions remain downloadable and are not yanked for migration. See the completed transition issue #48.

Install the new product explicitly with pipx install session-peer, uv tool install session-peer, or the standalone installer. No cc-peer command alias is installed. Both products can coexist. After checking your workflows, remove the old package with the same manager that installed it, e.g. pipx uninstall cc-peer. For a script installation, use the install.sh --uninstall from the pinned cc-peer v0.5.1 tag; check its paths and back up local customizations before running it. New uninstall only removes session-peer files.

Update scripts and agent instructions to call session-peer and use the new standalone program path ~/.local/share/session-peer/session_peer.py, not the old ~/.claude/skills/cc-peer/cc_peer.py. The new Claude skill lives separately in ~/.claude/skills/session-peer/. Claude/Codex configuration, session data and old installations are not migrated or removed automatically. Replace CC_PEER_REPLY_HOST with SESSION_PEER_REPLY_HOST when convenient; the old variable remains a fallback.

The final cc-peer release is not an ongoing feature or security-maintenance promise. The frozen root cc_peer.py is retained in tags for old self-update URLs but is excluded from the new wheel and sdist. Its local update command directs users here instead of installing a different product.

Claude Code sessions

Use the official feature first

For Claude-to-Claude workflows, consider Claude Code's built-in cross-session messaging and Remote Control first. session-peer provides a shell-driven local/SSH path when that workflow is not available or suitable, and a common CLI for Claude and Codex targets. The Claude-specific guidance in this section is not a prerequisite for Codex queues.

How the inbox transport works

Claude Code's session inbox socket accepts a JSON line:

{"type":"user","message":{"role":"user","content":"your message"}}

session-peer connects on the machine that owns that inbox. For SSH sends, it pipes its source to remote python3 - and makes the connection there, rather than forwarding the Unix socket. Native Windows targets use a named pipe and the auth line from the session's .key file instead.

Session records normally live in ~/.claude/sessions/<pid>.json and carry the socket path in messagingSocketPath. Never guess /tmp/cc-socks/: previous tests found both that layout and /run/user/1001/cc-socks/. A live PID without a bound inbox is treated as unreachable. The record schema is an internal interface and may change; use session-peer list --all to inspect stale or inbox-less records. A record or socket's existence does not guarantee that the caller has permission to write to it.

The receiving side decides what happens next

"Posted" is not "delivered." Writing to the socket succeeds; whether Claude ever reads the message is up to that session's inbound controls.

Messages can be held for approval or refused. In the legacy verification, a bypass-mode receiver held script-originated messages until approved. Do not infer receipt from a successful socket write or change permission modes to make a test pass.

If you deliberately want a worker to accept incoming messages unattended, configure that receiver explicitly:

{ "crossSessionInbound": "accept" }

Scope this with project settings or --settings if it is intended for one worker; user settings affect other sessions for that OS user too. Accepting messages can start receiving turns and consume usage. session-peer never applies this setting for you, and the receiver's own tool permissions still apply.

Why not tmux send-keys?

Terminal keystrokes can land in a running subprocess or permission prompt, not the intended chat input. session-peer uses the agent's inbox/queue boundary instead. Claude treats an inbox message as peer text, subject to its receiving-session rules, not as the user typing approval.

Limits

  • Sender identity is best-effort. When running inside a detectable Claude or Codex session, session-peer adds an agent-qualified textual From: header. Outside that context, it may omit it. This is not an authenticated identity protocol; environment variables and session registries are local hints.
  • Advertised replies need a working return path. When an agent sender and reply host can be determined, Reply-To and a compatibility Reply: line describe a return route. Forward SSH success does not prove reverse SSH access; use the opt-in doctor check. The address grants no access and session-peer does not correlate or wait for a reply.
  • Tailscale status is a local routing hint. A known online peer is addressed by its current MagicDNS name and a known offline peer is rejected before SSH. An unknown destination remains ordinary SSH; session-peer does not claim that every SSH host belongs to the tailnet.
  • No discovery across a bastion. --host is a single SSH hop; chain it yourself with an SSH config ProxyJump.
  • Destination user and execution permissions matter. Claude inboxes and Codex state/queues belong to the destination account. Use the correct account and home. A known_hosts entry does not store that account. A caller's sandbox may still deny access; session-peer does not bypass either agent's permissions or quota.
  • --host and --ssh-opt grant SSH access, not just messaging. Destinations must be single host aliases or addresses without whitespace or shell syntax. Options are allowlisted before any ssh invocation, including configuration inspection. Accepted flags are -4, -6, -p, -l, -i, -J (split or attached values), and -oKEY=value or -o followed by KEY=value. Allowed keys are Port, User, IdentityFile, HostName, HostKeyAlias, ConnectTimeout, BatchMode, ServerAliveInterval, ServerAliveCountMax, StrictHostKeyChecking, ProxyJump and IdentitiesOnly. Other options, including alternate config files, control sockets, providers and command hooks, are rejected. Your existing user/system SSH configuration remains trusted and can itself execute commands; this filter is not a sandbox for that configuration. Message bodies and session names are shell-quoted. Explicit StrictHostKeyChecking accepts only yes, ask or accept-new; no and off are rejected. Redirecting UserKnownHostsFile is not allowed. The stricter syntax rejects SSH URI jump targets, usernames containing a plus sign and space-separated option assignments; use host-style jump targets and equals-sign assignments.
  • Windows support. Claude's named pipe transport is supported. install.sh and the standalone remote installer/updater use POSIX shell; use a Python package manager on native Windows. Live Codex verification remains macOS-only; the Codex home, Reply-To, doctor, and JSON fixtures run in Windows CI.

Tests

python3 -m unittest discover -s tests -v

The default suite needs no live agent, SSH server or network. It includes legacy compatibility tests, shared CLI helpers, fixture-based Codex discovery and queue subprocess tests, and isolated standalone installation/coexistence checks. Codex coverage includes argv/payload handling, destination home selection, real advisory-lock probing, stable owner evidence, dry-run without dispatch, failure/timeout semantics and remote option forwarding. Multi-home fixtures reproduce duplicate UUIDs across the default and Orca/configured homes, unique/multiple/changing writer evidence, fail-closed inventory errors, explicit selection, alias deduplication, single-home compatibility and structured home/submission metadata. They never infer a live writer from a saved row or submit messages to real sessions. Update-notice coverage checks freshness and expiry, strict stable versions, atomic private writes, single-flight background refresh, opt-out behavior, package-manager guidance, multi-host scope, and failure isolation.

CI runs tests on Ubuntu/macOS with Python 3.9 and 3.13, and Windows with Python 3.13 (POSIX installer tests are skipped there). Separate jobs check shell syntax with shellcheck, standalone install/reinstall/uninstall, and wheel/sdist contents and installation. This is not full transport coverage: Claude discovery fixtures, real UDS payload checks and broader exit-code/remote-command regression tests remain tracked in #28.

Verified

session-peer v0.8.0 candidate (2026-09-16)

  • Integrates unified Claude/Codex listing, optional MCP tools and a Codex plugin, explicit bounded Codex wake, multi-home Codex listing, and named message and output formatting options from #76, #77, #78, #80, and #82.
  • See v0.8.0 release notes for JSON compatibility, optional dependencies, the Codex 0.154.0 wake boundary, and validation limits.
  • 301 local tests passed with the MCP SDK; standalone runs skip two optional SDK tests. Wheel and sdist installed independently and report v0.8.0.
  • Release preparation does not publish a GitHub release or upload to PyPI.

session-peer v0.7.0 (2026-09-16)

  • 240 local tests passed for cached update notices, the shared JSON envelope, read-only diagnostics, structured Reply-To parsing/routing, same-machine normalization, and opt-in reverse-route classification.
  • CI covers Ubuntu and macOS with Python 3.9/3.13, Windows with Python 3.13, package build and isolated installs, standalone install smoke tests, shellcheck, and secret scanning.
  • Wheel and sdist contents were inspected and installed independently. Both artifacts report v0.7.0 and exclude the frozen legacy cc_peer.py.
  • A real SSH doctor run found Claude inboxes and bounded default/Orca Codex homes. Its opt-in reverse probe reported authentication failure independently of the successful forward connection. No live message was submitted.

session-peer v0.6.2 (2026-09-15)

  • 198 local tests and the release build checks passed for UUID-specific Codex writer validation, same-machine reply localization, SSH destination-user resolution, and structured connection-failure diagnostics.
  • Wheel and sdist contents were inspected and installed independently. Both artifacts report v0.6.2 and exclude the frozen legacy cc_peer.py.
  • No live message was submitted during release preparation. Queue acceptance, consumption, acknowledgements, and reverse SSH reachability remain distinct outcomes.

session-peer v0.6.1 (2026-09-15)

  • 174 local tests and the release CI checks passed after integrating the v0.6.0 Codex adapter with duplicate-home protection, sender-agent envelopes, and MagicDNS SSH routing. Wheel/sdist builds and isolated installs were checked.
  • A local Codex listing and explicit-home dry-run succeeded without submission. A read-only SSH listing supplied by Tailscale IP connected through the current MagicDNS HostName and retained the original value as sshHost/HostKeyAlias.
  • No live message was submitted as part of release preparation. Queue acceptance, message consumption, acknowledgements, and reverse SSH reachability remain distinct outcomes.

session-peer v0.6.0 transition (2026-09-10)

  • 134 local tests and the release CI checks passed. Wheel/sdist builds and isolated installs, actual PyPI installs, package-manager update protection, and coexistence/removal of the legacy CLI were checked.
  • With Codex CLI 0.154.0 on two macOS machines, local/SSH saved-session discovery, dry-run and actual queue submission worked. The submitted bodies matched the queue records. Those checks established queued, not consumed or acknowledged, and did not establish a reliable active-session indicator.
  • Claude local/SSH inbox writes through the new CLI succeeded. A weekly usage limit prevented fresh receiving-turn/reply verification; those writes are not claimed as completed round trips.
  • Standalone CLI/Claude skill migration was checked on two macOS hosts and two Ubuntu hosts. Installation and read-only discovery checks did not start agent turns or change inbound settings.

See RELEASING.md and #48 for the release sequence and verification limits.

Historical cc-peer Claude transport verification

Before the rename, Claude Code v2.1.263 was exercised across five machines over SSH on a Tailscale network: two macOS 26 (Apple silicon), two Ubuntu 24.04 (arm64, Oracle Ampere A1 in separate regions), and one Windows 10 22H2. These historical observations are not a claim that every test was repeated with session-peer v0.6.0:

  • Posting from macOS to Linux sessions in two regions; each landed in the receiving transcript as type: user with origin.kind: "peer".
  • Payload integrity — quotes, backticks, $HOME, and emoji arrive byte-for-byte.
  • The held path: a bypass-mode session raised an approval dialog, then logged Released 1 held cross-session message once approved.
  • Both socket layouts in the wild: /tmp/cc-socks/ on one Ubuntu host, /run/user/1001/cc-socks/ on another running the same build.
  • Windows named pipe transport (\\.\pipe\LOCAL\cc-msg-<hash>) with mandatory auth line read from the session's .key file. list, send, and --host all verified on the Windows machine.

Both agents' discovery formats can change between upstream releases. A previous successful transport test does not guarantee discovery or delivery in a newer agent build.

License

MIT

Combined session discovery

Default list and list --json query all registered adapters: Claude, Codex, and live registered Antigravity bridges. Use --agent claude, --agent codex or --agent antigravity to select one. Every session row includes agent: "claude" | "codex" | "antigravity"; agent-specific fields such as PID and thread UUID remain unchanged. Combined human output includes an AGENT column. Claude rows precede Codex rows, preserving Claude discovery ordering; Codex rows sort by descending update time, then home and UUID.

List responses include discovery, keyed by each requested agent, with status: "ok" | "not_installed" | "error" and an error explanation for failed sources. For Antigravity, not_installed with no_live_registration means no live registered bridge was found; it does not establish whether the CLI is installed. Any discovery failure returns ok: false, a top-level error summary, and exit code 1 while retaining successfully discovered sessions. Automatic Codex discovery with no readable state database and no inspection errors is a normal empty result (not_installed, exit 0); a missing explicitly configured home is an error. A missing Claude sessions directory is an empty result. Discovery results alone are not proof that a Codex process is running. Malformed individual Claude records continue to be skipped as before.

These semantics apply locally and over SSH. Repeated hosts retain independent results in the existing ordered array; any failure makes the overall exit code 1. Codex listing includes default, environment-selected, Orca, and configured homes. Use --codex-home PATH to inspect only that home. --all retains Claude stale/no-inbox records and includes archived Codex threads.

Optional MCP / Codex plugin

For structured, destination-restricted list_sessions and send_message tools, install session-peer[mcp] with Python 3.10+ and follow MCP setup. The default policy permits local listing only. The standalone CLI and shell installer retain their existing dependency requirements.

For opt-in activation of queued Codex sessions, see explicit wake.

See multi-home Codex listing for candidate sources, per-home errors, duplicate UUIDs, and selecting the exact home for send.

Internal extension architecture

Agent adapters and local/SSH execution share an internal versioned contract while retaining the single-file CLI. See adapter development and the architecture decision. External plugin loading is not available. doctor.capabilities.agents describes implemented list/send/wake/wait/ack support; it does not grant permission or prove readiness.

install.sh stages local and SSH runtime updates in the destination filesystem, downloads both runtime and skill first, and validates the staged CLI version before atomically replacing the runtime. Download or validation failure leaves the installed runtime, launcher and skill intact. This is not a transaction across all installation files, a signature check, or rollback after a later skill-manager failure. Existing skill-manager ownership is unchanged.

SSH validates the final shell-quoted remote command as UTF-8 bytes before starting any SSH subprocess. The conservative limit is 131071 bytes, including base64 message expansion, envelope, options and quoting. Oversized commands fail locally with ssh_command_too_large and submitted: false; character-count validation alone is insufficient for Unicode. Local delivery limits are unchanged. This per-command guard cannot guarantee that every remote operating system has enough total environment/argument space.

At native delivery, session-peer quotes every peer-body line and marks sender/session claims and reply routes as unverified. Codex and Antigravity also receive an explicit warning that peer text is not user permission; Claude Code already provides that warning, so it is not repeated. The --no-from and --no-reply-to options suppress sender metadata, not this receiving-side frame. Reply-To remains inert routing data: verify the surrounding frame and confirm a third-party destination with the session owner before replying. Framing reduces ambiguity, not all prompt injection. The frame and escaped control characters count toward message limits; chars describes the delivered framed text. Submission and consumption/ACK meanings do not change.

Connection-bound SSH key identity (development)

The opt-in --ssh-identity flag reports sshIdentity for list, send, and doctor --host. Only the fingerprint and key algorithm come from the actual OpenSSH host-key callback followed by a complete validated remote result. The destination, port and key-lookup name are preflight SSH configuration, not independent proof of the final network endpoint. An offered key without a completed remote result is only observed, not verified.

session-peer list --host workstation --ssh-identity --json
session-peer send --host workstation --to reviewer --message="Review this change" \
  --require-ssh-host-key 'SHA256:<previously-verified-fingerprint>' --json

Copy the verified fingerprint from discovery; the placeholder above is not a valid key. The pin is checked on the send connection, before authentication and remote execution, rather than by a separate probe. A changed key is refused; key rotation requires explicit verification and a new pin. Connection reuse is disabled in this mode and strict host-key verification stays enabled. In report mode the user's known-hosts trust is retained; pin mode trusts only the supplied public-key fingerprint for that connection and writes no known-hosts entry. A server key can be reused by multiple hosts, so it is not a unique machine, account, session-generation or message-consumption identity.

This development path is POSIX-only and supports ordinary Ed25519, RSA and ECDSA keys, not host certificates or security-key algorithms. It refuses a user-configured KnownHostsCommand instead of silently replacing that trust policy. Existing SSH configuration and jump-host trust remain operator-owned. The optional second installed-version probe is omitted for identity-mode listing because its separate connection is not covered by the receipt. Missing receipt after a complete native response preserves that submission evidence, never permits automatic retry, and cannot satisfy a required key pin.

Default output and SSH behavior are unchanged without opt-in. These options are not part of published v1.0.4. Local tests cover the native callback helper, read-only configuration, fake transports and actual OpenSSH handshakes against an owned loopback fixture. A mismatched key caused zero authentication and remote-exec requests. Fixture host private keys remain in memory, never files. This is not a real remote-host or live-agent acceptance test; platform CI must also pass.