Skip to main content

session-peer

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.

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 sessions (default)
session-peer list --agent claude                # explicit equivalent
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"     # Claude name or PID
session-peer send --to 'codex:<full-thread-uuid>' "message"
session-peer send --host worker --to 'codex:<full-thread-uuid>' --dry-run "message"

Replace <full-thread-uuid> with a full ID from the destination's Codex listing. list selects one agent at a time; it does not merge both agents. Use --agent codex, 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, and session-peer does not wake/resume a session or wait for its reply.

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 Claude Code skill. To add the skill so Claude can use session-peer on its own:

mkdir -p ~/.claude/skills/session-peer
curl -fsSL -o ~/.claude/skills/session-peer/SKILL.md \
  https://raw.githubusercontent.com/abruption/session-peer/main/skills/session-peer/SKILL.md

install.sh

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 the Claude Code skill in ~/.claude/skills/session-peer/, and links ~/.local/bin/session-peer. Existing cc-peer files are preserved. Remove it with ./install.sh --uninstall [--host ...].

session-peer update refreshes a standalone program from the latest GitHub release. ./install.sh --host <host> pushes this checkout's 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.

Or skip the installer entirely and copy the one file:

curl -O https://raw.githubusercontent.com/abruption/session-peer/main/session_peer.py
chmod +x session_peer.py

The skill

The standalone installer puts the program in ~/.local/share/session-peer/ and the skill separately in ~/.claude/skills/session-peer/SKILL.md. Skill placement respects CLAUDE_CONFIG_DIR, then ANTHROPIC_CONFIG_DIR, before the default. The skill guides Claude's choice of target and message; the Python program performs discovery and transport. Installing it does not install a Codex plugin or change either agent's permissions or inbound settings.

Usage

session-peer list                                  # Claude sessions on this machine
session-peer list --host web-01                    # Claude sessions 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 send --to api-worker "message"        # local session
session-peer send --host web-01 --to api-worker "message"
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 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: 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.

Repeat --host to operate on several SSH destinations. --json is available on list, send, and update; v0.6 retains command-specific response shapes rather than a uniform envelope. See #29.

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.

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.

Updating

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 (including --check) prints package-manager guidance without checking GitHub or replacing the package's 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 even if the remote version is newer; 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.

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 before an implicit send/dry-run. JSON array of absolute paths (or ~/…), not a shell command or a path-separated list. It does not change the selected home or merge listings.

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.

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.

Use the same explicit destination home for list and send. 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. --codex-home explicitly chooses that copy but is not proof of activity. This explicit-home workaround also works with the published 0.6.0 package.

The #55 safeguards described below ship in v0.6.1; the original PyPI 0.6.0 package does not check other homes or expose the new JSON fields.

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.

If the UUID occurs in multiple known homes, the command fails before queueing and names the candidates. Archived copies count too. During multi-home checks, unreadable or incompatible known databases and missing configured additional databases prevent implicit submission; an unreadable Orca inventory also blocks it. Choose --codex-home explicitly to bypass the ambiguity guard and unrelated inventory errors. Resolved symlink aliases and repeated paths count as one home.

The guard never automatically switches to another home. With no competing home, native queue behavior is preserved; list still reads only the selected home, so a successful listing is not an ambiguity check. There is no general filesystem scan, process-environment inspection, activity detection or atomic cross-home snapshot. Unconfigured/custom layouts and copies created after the check can be missed. For Orca layouts other than the macOS path above, configure the additional homes or use --codex-home. Further diagnostics belong to #45.

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 retains sessions and version, and adds the resolved absolute codexHome at the top level. Each entry has agent, id, name (first line, at most 120 characters), cwd, updatedAt (Unix seconds), and archived. Remote list JSON is an array of per-host results, even for one host; each successful result includes host, sessions, version, codexHome and an optional remoteVersion for an installed standalone copy.

Codex send JSON retains target: {agent, id}, status: queued (or validated under dry-run), ok, chars, dryRun, and optional queueId. It adds:

  • codexHome: the resolved absolute destination home, not a sender-side guess.
  • submitted: true only after successful queue CLI completion, false for dry-run.
  • consumptionConfirmed: always false; neither queued nor validated establishes consumption.

Errors keep the existing {ok: false, error} shape (plus host on remote results). 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. The changes are additive, not the cross-command envelope redesign tracked in #29.

A remote send to one host returns a flat object with host; multiple hosts return an array. Claude output stays compatible. When CODEX_THREAD_ID (or the compatibility fallback CODEX_SESSION_ID) is present, the message envelope and reply command identify the originating Codex thread.

doctor, reply waiting/--wait, structured reply URIs and optional wake are future work, not v0.6 features: see the v0.7 and v0.8 roadmaps.

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, a Reply: line supplies an SSH return command. Forward SSH success does not prove reverse SSH access. v0.6 does not verify that route, correlate a reply, or wait for one; the address grants no access.
  • 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 caller's sandbox may still deny access. session-peer does not bypass either agent's permissions or quota.
  • --host and --ssh-opt are as trusted as your ssh config. They are handed to ssh, so whoever controls them controls where you connect. Values that would make ssh run a local command (ProxyCommand and friends) are refused, and a --host starting with - is rejected outright — but if you allowlist session-peer for an agent, treat it as granting SSH, not just messaging. Message bodies and session names carry no such risk: they are quoted before they reach any shell.
  • 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 for v0.6 was on macOS, not Windows.

Tests

python3 -m unittest discover -s tests -v

The 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, dry-run without dispatch, failure/timeout semantics and remote option forwarding. Multi-home fixtures reproduce duplicate UUIDs across the default and Orca/configured homes, fail-closed inventory errors, explicit selection, alias deduplication, single-home compatibility and home/submission metadata. They never infer a live writer from a saved row or submit messages to real sessions.

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.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

Release files for session-peer 0.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for session-peer 0.6.1
File Size Uploaded
session_peer-0.6.1.tar.gz 37.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for session-peer 0.6.1
File Interpreter ABI Platform
session_peer-0.6.1-py3-none-any.whl Python 3 none any Details

Total release size: 66.8 kB

Release files / session_peer-0.6.1.tar.gz

Download URL session_peer-0.6.1.tar.gz
Size 37.4 kB
Tags Source
SHA-256 checksum
How to use checksums
815e320153a0e45c1c07190af0d956e0da2f856c57b1891d6744e01ce49f462e
BLAKE2b-256 checksum
How to use checksums
7266a52fb732d6136d0c5a8bd5081f6ace4d00df8e82d39a211066e963b4b909
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / session_peer-0.6.1-py3-none-any.whl

Download URL session_peer-0.6.1-py3-none-any.whl
Size 29.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a81aa8f9c1b0ce8be6cce50e4d924ea61b74183a86309f1f6ddcab9a396bc6c3
BLAKE2b-256 checksum
How to use checksums
cce63ca89e0934cb65260c3ac5a6c753207500711fbdc687a1b0104cf1f21a0f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page