Repository navigation
Spike: can opencode-skein become a native Claude Code peer? #80
Description
Proposal
Why
The user wants same-machine, bidirectional discovery and messaging between opencode-skein
sessions and Claude Code sessions — opencode-skein visible to stock Claude Code's own
ListAgents/SendMessage, and vice versa, with no plugin, no MCP, no bridge, and no daemon
required on Claude's side. That is the hypothesis worth testing: Claude Code already has a local
cross-session peer protocol (registry file + Unix-domain socket + NDJSON wire format); if
opencode-skein can register as a compatible peer on it, discovery and transport are solved for
free in both directions, with one protocol instead of two merged registries.
This is unproven and must not be assumed. Claude's local peer protocol is not a documented
public API. Everything about it — file locations, schema, auth/token handling, PID/process-start
identity requirements, wire format, receipts, priority semantics — must be verified against the
actually-installed Claude Code version on this machine before any production code is written
against it. The detailed research checklist the user provided (registry fields, socket security
expectations, auth token derivation, PID/proc-start identity questions, wire protocol frame
shape, attribution envelope format, five specific spikes) is preserved in design.md rather than
repeated here.
This repo already has a mature, deliberately-non-daemon approach to same-machine coordination
(fleet-instance-presence uses mDNS + per-instance HTTP; agent-coordination-bus, an MQTT push
transport, is explicitly gated as speculative until presence proves the polling ceiling). Whatever
this spike finds must be reconciled with that existing direction, not built as a third, disconnected
mechanism — see Non-Goals.
What Changes
This change produces findings and disposable spike code, not production architecture. Per the
user's explicit direction: research first, prove the hypothesis with throwaway spikes, then decide.
Five spikes (detail in design.md)
- opencode/test process → stock Claude Code, via Claude's native socket.
- Stock Claude Code
ListAgents/SendMessage→ a fake but protocol-compatible opencode peer.
This is the load-bearing spike — if this doesn't work reliably, the entire premise fails
and the change should stop here with a clearly documented reason, not fall back to a hidden
bridge protocol. - Round trip: opencode → Claude → reply → opencode.
- opencode ↔ opencode over the same compatibility mechanism — reframed:
peer-messaging
now delivers opencode-to-opencode messaging independently, over opencode's own HTTP API, not
gated on this spike. Spike 4's question is therefore narrower than originally scoped: not "is
opencode↔opencode messaging possible" (already yes, viapeer-messaging) but "would
consolidating onto the Claude-compatible mechanism be worth replacing that direct path" — a
question to answer, not a requirement to satisfy for opencode↔opencode to work at all. - Busy-recipient semantics: a message arriving mid-turn must not corrupt the active turn.
Deliverables
- A written, dated record of the verified protocol (or the specific point it broke down),
including the exact Claude Code version tested against. - Disposable spike code in an isolated location (not wired into the real session/plugin system),
using temporaryCLAUDE_CONFIG_DIR/XDG_RUNTIME_DIR, never touching real Claude sessions. - A go/no-go recommendation for
claude-peer-protocol(the gated implementation change), including
the PID/process-identity design question (one process per addressable session vs. a per-peer
sidecar process vs. some other mechanism Claude's registry actually tolerates).
Non-Goals
- No production registry, socket server, or tool surface — that is
claude-peer-protocol, and it
does not proceed past a stub until this spike's findings land. - No remote/federation, no MCP, no A2A, no central broker, no llama-skein or specsync messaging
integration — all explicitly out of scope per the user's direction. - Not superseding
fleet-instance-presence/agent-coordination-busfor opencode-to-opencode
presence — Spike 4 investigates whether the Claude-compatible mechanism could serve that need
too, but does not commit to replacing the existing mDNS/HTTP approach without a separate
reconciliation decision after findings land.
Prior Art Found In This Repo
packages/opencode/src/plugin/skein-peers.ts exists (untracked in git, not registered in any
plugin loader) — a rough, non-working sketch of list_agents/send_message tools (hardcoded
localhost:4096, a broken relative-path fetch for a local JSON file). Its actual disposal and
the harvesting of anything reusable from it is peer-messaging's job, not this spike's — that
change targets the exact same-machine opencode-to-opencode messaging problem this file sketched,
without depending on Claude compatibility at all. This spike references it only for context.
Dependencies
- None required to start. Runs independently of every other change in this plan.
peer-messagingis not a dependency of this spike, but the two are closely related: once
peer-messagingships a harness-agnosticPeer/PeerMessagemodel for opencode-to-opencode,
this spike's findings determine whetherclaude-peer-protocolextends that same model with a
Claude-Code-reachable harness, rather than building a separate one.- Gates
claude-peer-protocol(implementation) — that change does not get real tasks until this
one reports findings.
Impact
- No production code paths touched. Spike code lives under an isolated scratch location (e.g.
packages/opencode/spike/claude-peer/or the session's scratchpad — notpackages/opencode/src/)
and is deleted or explicitly promoted, never left half-wired. - Research touches read-only inspection of
~/.claude/config/session state on this machine —
read-only, isolated temp dirs for anything destructive, per the user's explicit instruction.
Disposition (2026-09-18)
Shipped. GO verdict acted on: claude-code-peer-source + claude-peer-messaging shipped the peer end to end. Unexecuted slices 3.1/4.1/5.1 were verified live by the sidecar implementation instead.
Design notes
Design notes: Claude Code peer-protocol research
Everything below is a research checklist, not a set of confirmed facts. Every field, path, and
behavior named here is a hypothesis to verify against the actually-installed Claude Code version
(claude --version) on this machine, and against the reverse-engineered reference implementation.
Do not hard-code any of it into production code before it is verified. Where verification is
impossible (undocumented, changed, or ambiguous), say so explicitly rather than guessing.
Prior art to study first
github.com/PeterSR/claude-code-socket-transport— reverse-engineered Claude local cross-session
inbox protocol. ReadREADME,paths.go,message.go,client.go,inbox.go,auth.go,
addr.go,platform_*.go, and its tests. Understand the protocol; do not copy code verbatim.github.com/anthropics/claude-code— the real client. Cross-check behavior against it.Jesse-njx/dsh-crosstalk— a different, file-inbox/heartbeat-based cross-session tool. Useful for
identity, stale-peer handling, provenance, and busy/idle delivery ideas, but its transport is
deliberately not what this spike is testing (Claude-native compatibility, not a parallel inbox).- This machine's live Claude Code install and (read-only, non-destructive) observation of real
session state under locations that may include~/.claude/sessions/,$XDG_RUNTIME_DIR/cc-socks/,
$CLAUDE_CODE_TMPDIR/cc-socks/— exact paths TBD by research, never mutate live files.
What to verify about Claude's current cross-session model
Session registry — exact location (candidate: $CLAUDE_CONFIG_DIR/sessions/<pid>.json, falling
back to ~/.claude/sessions/<pid>.json), exact schema, required vs. optional fields, which fields
ListAgents actually reads, how reachability/state/display-name are determined, how renames
propagate, how staleness is handled, how cwd is represented, how peer-protocol version is
advertised, whether PID ownership and process-start time are verified, and whether the registry
PID must match the process actually accepting the socket connection.
Inbox socket — path shape (candidate: <runtime>/cc-socks/<pid>.sock), directory/socket
ownership and mode (candidate: dir 0700, socket 0600 — verify, never weaken to ease
compatibility), macOS vs. Linux differences, path-length behavior, uds: address encoding,
fallback paths.
Wire protocol — newline-delimited JSON, one frame per line; candidate message shape includes
msgV, msg_id, type, message.role/message.content, priority, from, session_id,
plus control/receipt/rename/auth frame types. Verify against the real transport and real Claude
behavior, not this list. Implement a strict codec that rejects malformed/oversized/unsupported
frames.
Auth/peer tokens — candidate key file shape <claude-config>/sessions/<pid>.<socket-hash>.key
containing something like {"peerToken": "...", "procStart": "..."}. Verify filename rule, hash
input, token format, when auth is required, what a sender does with a target's token, how Claude
classifies authenticated vs. unauthenticated senders, whether our peer must publish this file to
behave natively, and whether Claude's sender expects target PID/proc-start to match a real process.
Implement real validation — never disable it to pass a test.
Process identity — Claude's model appears tied to PID + kernel process-start identity. Map the
real opencode-skein relationship: one process may host multiple sessions; do not assume 1:1 or
assume the opposite. If Claude verifies PID/proc-start against a real process, do not invent fake
identity — evaluate: (A) each opencode-skein session already has its own process, use it directly;
(B) a small per-peer sidecar process that legitimately owns a PID/socket per session; (C) some
other mechanism Claude's registry actually tolerates. Choose the simplest option that preserves
correct identity and security, and state explicitly which one was chosen and why.
Session identity — do not use PID as opencode-skein's permanent identity. Claude's own PID can
outlive a logical session (e.g. across /clear) or vice versa. Map: opencode session ID →
Claude-compatible runtime registration (PID, procStart, socket, compatible session ID, compatible
display name) — a translation layer, not a replacement of opencode's canonical session ID.
Return address / replies — verify exactly how a Claude receiver gets enough information to
reply to an opencode sender (from, from-name, registry identity, message IDs, receipts). A
one-way send that cannot be replied to fails the actual goal (bidirectional conversation).
Receipts / delivery status — verify Claude's receipt concepts (candidates: held, denied,
expired, delivered). opencode-skein must distinguish at minimum: socket-accepted, queued
locally, injected into target session, rejected, unreachable — and must never claim "delivered"
when it only knows write() succeeded.
Priority — candidates now/next/later. Verify wire representation. Priority affects
scheduling only, never authorization — a now message must not bypass permissions.
Attribution envelope — Claude reportedly expects a tagged <cross-session-message ...>... </cross-session-message>-shaped envelope for correct sender attribution, with careful escaping.
If confirmed, reproduce it exactly at the compatibility boundary only; normalize into an internal
peer message immediately after decoding so Claude's wire format never leaks into the rest of
opencode-skein. Message content must never be able to forge sender identity or envelope
boundaries.
Security requirements (non-negotiable, verify Claude's actual expectations and match them)
Same-OS-user scope by default. Defend against: other local users, forged registry files, socket
replacement/symlink attacks, stale PID reuse, world-writable runtime directories, forged auth/key
files, message replay, malformed JSON, oversized frames, message flooding, duplicate message IDs.
If the expected runtime directory has insecure ownership/mode, fail closed with a clear diagnostic
— never silently weaken permissions to make the compatibility easier to hit.
Inbound policy (design question to answer, not just implement)
A peer message is agent context/request, never a permission grant — normal opencode tool
permissions stay authoritative regardless of what a Claude peer's message says. Design at minimum
accept/hold/refuse semantics, analogous to Claude's own inbound policy concept.
OpenCode-side integration questions to research against the current plugin/session API
Not the old API — inspect what this repo's current packages/opencode plugin/session layer
actually exposes: session listing/lookup/hierarchy/status, session.synthetic(...),
session.prompt(...), session lifecycle events (session.created/updated/idle/status/deleted).
Specifically determine whether session.synthetic(...) can wake/resume an idle session, can be
queued for a busy one, and retains explicit provenance (peer name, harness, message id) — this is
the same wake-up primitive subagent-notification-reliability is independently investigating for
background subagents; if that investigation finds the injection path fundamentally unreliable, this
spike inherits that problem for inbound Claude messages too and should say so.
Definition of done for this spike (see proposal.md and tasks.md)
A written, dated finding per spike (1-5), a go/no-go recommendation, and — if go — enough of a
verified protocol description that claude-peer-protocol can be planned as real, scoped tasks
instead of another research change.
Tasks
Slice 0: Ground truth
- 0.1 Record installed Claude Code version (
claude --version) and platform (macOS/Linux) —
every finding below is scoped to this exact version.- Done 2026-09-05: 2.1.261, darwin (macOS). See
findings.md.
- Done 2026-09-05: 2.1.261, darwin (macOS). See
- 0.2 Read
PeterSR/claude-code-socket-transport— not needed in practice: this
machine's own live registry (six real concurrent sessions) was fully self-explanatory and
directly inspectable, and gave exact, verified answers the reference implementation could
only have offered as a starting hypothesis. Seefindings.mdfor what was verified
firsthand instead. - 0.3 Read-only inspection of real local Claude session/registry/socket state on this machine.
Never mutated live files during inspection.- Done 2026-09-05. Full schema, permissions, and hashing scheme recorded in
findings.md.
- Done 2026-09-05. Full schema, permissions, and hashing scheme recorded in
Slice 1: opencode/test process → stock Claude
- 1.1 / 1.2 Subsumed by Slice 2: a genuine
SendMessagecall's real wire frames were
captured verbatim by a disposable listener (see Slice 2), which is the same evidence Slice
1 would have produced from the sending side. Not separately re-run as a standalone
hand-rolled client — seefindings.md, "Not tested in this spike".
Slice 2: stock Claude → fake opencode peer (load-bearing)
- 2.1 Registered a disposable fake peer (
claude-peer-spike-test) using a genuine throwaway
process (sleep, real PID/procStart) — done against the real, shared registry with
explicit user permission for this specific test, after an isolatedCLAUDE_CONFIG_DIR
attempt hit an auth requirement that made it impractical for this narrow test. Full
before/after cleanup verified (registry restored to exactly its prior 6-session state).- Validation: passed — unmodified
ListAgentslisted it asinteractive · idle.
- Validation: passed — unmodified
- 2.2
SendMessagetargeting the fake peer; disposable listener received and decoded the
real frames.- Validation: passed, on the second attempt — the first attempt used a single-shot
listener that (correctly) got treated as unreachable afterListAgents' own liveness probe
consumed its one handled connection; a persistent listener then captured the full real
auth + message frames. Seefindings.mdfor the exact bytes.
- Validation: passed, on the second attempt — the first attempt used a single-shot
- 2.3 N/A — 2.1 and 2.2 both passed outright; no failure to document.
Slice 3: round trip
- 3.1 Not executed — the reply mechanism (connect to the
fromURI, present that session's
own peer token, send the identical frame shape) is fully confirmed by Slice 2's captured
frames, but a live reply was deliberately not sent, to limit further writes to this
machine's live session state once the load-bearing question (Slice 2) was answered. Real
implementation work, not a remaining spike uncertainty.
Slice 4: opencode ↔ opencode over the same mechanism
- 4.1 Not executed. Nothing observed contradicts it — the fake peer in Slice 2 used no
Claude-specific internals, only file/socket conventions any process (including a future
opencode-skein sidecar) can implement identically on both ends.
Slice 5: busy-recipient semantics
- 5.1 Not executed.
Slice 6: findings and recommendation
- 6.1 Findings and recommendation written: GO, with Option B (per-session sidecar
process) identified as the correct process-identity model for opencode-skein — Option A
(Claude's own model, confirmed 1-process-per-session) doesn't fit opencode's
one-server-many-sessions architecture. Seefindings.md. - 6.2 All spike artifacts (disposable listener script, registry/key/socket files, throwaway
process) fully removed and verified —~/.claude/sessions/confirmed back to its exact
pre-spike file count,ListAgentsconfirmed back to the exact pre-spike peer list. Nothing
was ever added topackages/opencode/src/. - 6.3
packages/opencode/src/plugin/skein-peers.tswas already deleted inpeer-messaging
before this spike ran.
Unchecked items above: see Disposition in proposal.md (2026-09-18).
Plan changes
10 done
Activity
androidand commentedon Sep 17, 2026
Archived 2026-09-18 — see the change's Disposition section.