Staying in lock-step with the reference daemon¶
claustrum is behaviorally compatible with a reference daemon that ships inside Claude Desktop's SSH-remote feature. That daemon is versioned by git SHA and distributed as per-platform zstd blobs on a public CDN. This doc is how we detect when a new build appears and whether it changed anything we need to match. For the running history of which builds changed what, see the reference build ledger.
How the reference is distributed¶
- Per-version manifest:
https://downloads.claude.ai/claude-ssh-releases/<sha>/manifest.json→{"version":"<sha>","platforms":{"<goos>-<goarch>":{"checksum":"<sha256 of .zst>","size":N}}} - Per-platform artifact:
https://downloads.claude.ai/claude-ssh-releases/<sha>/<goos>-<goarch>/claude-ssh.zst - Six targets:
linux-amd64,linux-arm64,darwin-amd64,darwin-arm64,windows-amd64,windows-arm64. (GOARCH naming —amd64, notx64.)
There is no "latest" index — releases are keyed by SHA, so step 1 is always "find the new SHA."
Step 1 — find a candidate SHA¶
The reference build's SHA is whatever Claude Desktop currently deploys. Sources, easiest first:
- A host Desktop has connected to — the daemon is cached per-SHA:
- The Desktop app bundle — the pinned SHA (and all six per-platform
checksums/sizes) is a build-time constant baked into the app, readable
offline without connecting anywhere. In the Linux
.debit is aJSON.parse('{"version":"<sha>",…,"baseUrl":".../claude-ssh-releases"}')literal insideresources/app.asar(.vite/build/index.js); a parallel literal pins the CLI (claude-code-releases). So a new Desktop build is itself the "new SHA" signal. Observed: Claude Desktop for Linux 1.18286.0 (2026-07-02) pins7c2f88d…. - The Desktop machine's cache — per-platform binaries under
<app-data>/claude-ssh-remote/<sha>/(%APPDATA%/Claude/…on Windows), alongside a.verified-<goos>-<goarch>marker. - Probe a guess —
manifest.jsonreturns 200 for a real SHA, 404 otherwise.
Note: the CLI release manifest (
claude-code-releases/<ver>/manifest.json) has acommitfield, but that is the CLI's commit, not the daemon's — don't confuse them.How the client picks the deployed SHA (and a claustrum divergence). Before a session the client runs
server --versionon the cached~/.claude/remote/srv/<pinned-sha>/serverand matches/claude-ssh\s+(\S+)/; it skips re-upload only when that token equals the pinned SHA. The reference printsclaude-ssh <sha> (built …), so it hits the cache. claustrum printsclaustrum <ver> (built …)— its own identity and own version — so the token never matches the client's pinned SHA and the daemon is re-SFTP'd (idempotently, ~2.3 MB) every session. This is a consequence of theclaude-ssh:→claustrum:rebrand: it is CLI stdout, not a JSON-RPC frame, so the wire contract is untouched and the redeploy is harmless — the daemon that ends up running is still claustrum.To make claustrum a permanent drop-in (client sees it as up-to-date and stops overwriting it),
-versionneed only emitclaude-ssh <pinned-sha>as its first token — the client captures just that token, so a(via Claustrum <ver>)suffix is fine — with the binary placed at~/.claude/remote/srv/<pinned-sha>/server. The SHA is per-Desktop-build, so a drop-in build must track it (source it fromscripts/UPSTREAM_SHA) and move to the newsrv/<sha>/path when Desktop bumps. This is off by default (claustrum keeps its own identity); it would be an opt-in build stamp, not a wire change.
Step 2 — run the drift check¶
scripts/check-upstream.sh <sha>
# or, with the pinned baseline SHA in scripts/UPSTREAM_SHA:
scripts/check-upstream.sh
The script (network access required) will:
- Compare
<sha>to the pinned baseline inscripts/UPSTREAM_SHA; a difference is itself the "new release" signal. - Fetch the manifest, download the
linux-amd64claude-ssh.zst, verify it against the manifest checksum, and decompress it. - Build claustrum, then diff the two binaries on the things we must match:
- method names (
server.*/files.*/git.*/process.*literals), - CLI flags (
-helpoutput), -versionformat,- the app-facing string set (errors,
[Server]/[process.Manager]/[frameSink]/[shellenv]log lines, flag help). - Print
PASS(no drift in the checked surface) orDRIFTwith specifics, and exit non-zero on drift.
This static check needs no running daemon and is safe to run anywhere with network access.
Step 3 — authoritative byte-for-byte recheck (local only)¶
The static check catches added/removed methods, flags, and strings. To confirm
frame-level byte-identity (result field order, stream framing, error bodies,
reattach semantics), run the private validation battery (kept in scratch/,
not published) against both the new reference and claustrum:
# starts each binary as a PRIVATE -serve on a throwaway /tmp socket, runs the
# full method battery, and diffs normalized frames. Never touches a live daemon.
scratch/probe/validate.sh <path-to-reference> > /tmp/ref.json
scratch/probe/validate.sh ./claustrum > /tmp/mine.json
diff /tmp/ref.json /tmp/mine.json
Safety: only ever probe a private instance on a
/tmpsocket with its own-token-file; never point the harness at a live daemon's socket, and clean up every probe.
Step 3b — real-session capture (highest fidelity, optional)¶
Steps 2–3 drive the daemon with synthetic requests we author. The ultimate check is the real desktop client's traffic.
- The bridge (
server --bridge) is a dumb stdio relay, so teeing its stdin/stdout captures the exact client↔daemon NDJSON of a live session — which can then be replayed against claustrum and diffed. - Tooling lives in
scratch/capture/(gitignored): acapture-bridge,replay.js, andREPLAY.mdwith the capture runbook. replay.jsdiffs order-insensitively — responses keyed byid, stream frames byprocessId+seq— and masks the version SHA, the token, and (with--mask-data) the nondeterministic agent payloads.- The session is captured under a throwaway SSH user via a
ForceCommandwrapper scoped to that user, so it never touches a live daemon.
This was exercised against the then-pinned reference (8de85faaa…, since superseded by 7cbfa471): a real Desktop
session — 10 of the 18 methods, the full process.* lifecycle including a
32 KiB output stream and a mid-stream disconnect/reconnect that drove
process.reattach— verified byte-identical. Concretely, on real client framing:
- the live client uses only a subset of the 18 methods (no hidden method), and
every param shape it sends is one claustrum already accepts (incl.
process.spawnwith and withoutcwd/env); server.capabilitiesmatches exactly (version-masked), including the 18-method order;- the stream envelope is
{type,processId,stream,seq,data}(and…,exitCodeon exit) in that field order, with non-zero exit codes propagated verbatim; process.reattachis per-process withfromSeq; on reconnect the daemon replays buffered frames withseq > fromSeqand the sequence stays contiguous across the reconnect.
Use this as a periodic spot-check or when a new build changes process.*
behavior the synthetic battery can't fully model (real reconnect timing,
multi-process reattach). The raw dumps contain the session token and host paths —
keep them in scratch/ (gitignored); never commit them.
Step 4 — reconcile¶
If the check reports drift:
- Identify exactly what changed (new method? changed error string? new flag? new install fact? changed frame shape?).
- Implement the change in claustrum behind the existing structure, keeping every unchanged behavior unchanged.
- Re-run the static check and the byte-for-byte battery until both are clean.
- Update
scripts/UPSTREAM_SHAto the new SHA, note the change inCHANGELOG.md, and updatedocs/PROTOCOL.mdif the wire surface changed.
Not every claustrum behavior is meant to match the reference. A few deliberate, opt-in divergences — the
-cli-zstchecksum (D1), the CT-1wantPidpid/startTimefields, and the CT-2-keep-childrenserve flag — are catalogued inIMPROVEMENTS.md. They sit off the default path (the drift check and the synthetic battery never exercisewantPidor-keep-children, so they won't show as a diff), so don't "reconcile" them away as drift if a probe that opts in surfaces them.
Automating it¶
- A scheduled CI job (e.g. weekly
nightly.yml) can runcheck-upstream.shagainst the pinned SHA and open an issue if the manifest/strings/flags it can reach have changed. Because finding a new SHA needs an out-of-band source (above), the cron primarily guards against the pinned build being re-published or its surface shifting; feed it a freshly-discovered SHA to check a new build.