claustrum — deliberate divergences¶
Almost everything claustrum does is byte-identical to the reference daemon. This file is the canonical catalog of the exceptions. The exceptions are the behaviours that knowingly change a frame or an action. This file also gives the rules that gate every one of them.
The catalog table is the fast path. It gives one row per divergence, with its default, how to activate it, and its reopen trigger. The rules come first for a reason: they decide which shape an entry may take. The three shapes are always-on, opt-in, and conditional.
Per-method wire facts (params, result field order, error strings) live in PROTOCOL.md. Driver-claim provenance lives in ARCHITECTURE.md. We condensed the exhaustive per-entry measurement forensics out of the committed docs.
The one hard rule¶
Byte-identical JSON-RPC frames are the product. A divergence needs a reason. Matching does not need a reason. "Off by default" must mean byte-identical, not almost byte-identical. An opt-in divergence that is not active leaves the wire exactly as the reference leaves it.
PROTOCOL.md and the PR that shipped it also document every intentional divergence below. The out-of-scope list at the end gives the inverse: the changes we will never make.
How we decide (the rules)¶
The standard, in priority order:
- Claude Desktop must keep working. Claustrum is a drop-in. If a behaviour is reachable by Desktop and matching the reference is what keeps it working, we match — no matter how ugly the reference's behaviour is.
- Match by default. A wart Desktop tolerates is the contract, not a bug. Divergence needs a reason; matching does not. The burden of proof is always on the divergence.
- A divergence earns ALWAYS-ON only if either (a) the reference's behaviour on that path is not a frame at all — an unbounded wait, unbounded memory, or unrecoverable data loss — and no honest caller can observe the difference; or (b) the trigger itself is unreachable on an honest path; or (c) the trigger is reachable, but both binaries fail the operation and the only delta is diagnostic text.
- Anything else that changes an honest-path frame is OPT-IN, default off, or it does not ship.
Corollaries:
- Keeping a wart does not mean hiding it. We match the wart and we document it, so a user can see the edge before it causes damage.
- "The reference does it too" is a reason to match, never a reason to call it safe. D2 is the standing counter-example: the reference wipes a home directory, and we still refuse to do it.
- Every always-on divergence owes a reopen trigger. The reopen trigger is the observation that would make us take the divergence back out. An always-on divergence with no reopen trigger is a preference, not a decision.
Reading the clauses¶
Clause (a) is an AND. Both halves must hold: the reference's behaviour is not a frame, and no honest caller observes the difference. The second half is where thresholds fail. A bound is a threshold. A threshold cannot separate a hostile input from an input that is only slow or large, so an honest input trips it too. The test is this question:
Who pays when the guard fires on an honest input, and can they decline?
If the honest input is reachable, and the caller cannot turn the guard off, then always-on is not justified. This holds no matter how the reference behaves on the hostile path. This test flipped every timeout and size cap from always-on to opt-in (D3, D5, D10, D11, D12, D14; D4 is the non-threshold sibling).
Canonical example: D2 satisfies both halves. The reference's home-wipe is unrecoverable data loss, and no honest caller has a legitimate use for deleting home. A caller can still reach that path by accident, which is exactly what the guard is for.
Clause (b): the trigger is unreachable on an honest path. Most surviving always-on entries use this form (D6, D7, D8, D9). Each entry has its own trigger, and the glosses are not interchangeable. Two of the four are asserted rather than enumerated: Desktop's per-method param set has never been enumerated against D9's binding, and D6/D7 rest on an observed value plus a measured accepted-set. Read those two as unenumerated, not established (rule 2 puts the burden on the divergence).
Canonical example: D6 — a -cli-version naming a destructive path outside the
cli-dir is not something any correct client emits.
Clause (c): both binaries fail, and the only delta is diagnostic text.
This clause is deliberately narrow, and we wrote it for D13. Measured, D13 does
not meet it, so the clause justifies no entry in this file today. A reader should
not take two things on trust. First, an error.code is not diagnostic text,
because a client branches on it. Second, on-disk state that a caller can
files.stat is not diagnostic text either. D13's honest-path rows differ in both,
so the clause keeps its literal wording and D13 sits unresolved. We did
not widen the clause to fit its one candidate.
Canonical example: none currently qualifies.
The "Desktop owns the argv" premise¶
Every opt-in tag rests on one claim about the driver: Claude Desktop owns the
daemon's argv on both -serve and -install. Therefore an operator cannot reach
a flag-only knob, and the claustrum.conf key (read beside the executable) is the
reachable one. This claim is the premise under D3, D4, D5, D10, D11, D12, D14 and
under the "(opt-in)" tag itself.
The claim is load-bearing, so its provenance, its current evidence and its reopen
trigger are canonical in
ARCHITECTURE.md → Driver claims and their provenance.
That section also holds the other two driver claims (Desktop parses cliError;
Desktop uses the reported libc to choose which CLI build it downloads). The
claim reopens if a way for an operator to influence the daemon's argv is found, or
if a Desktop release adds one. Such a route would make a flag-only opt-in
sufficient for Desktop-driven hosts. It would not moot the config key, which
serves every other driver.
Conventions for opt-in divergences¶
These conventions hold for every opt-in entry. This section states them once rather than repeating them in each entry:
- A flag and a matching
claustrum.confkey. The config key is the reachable knob (see the argv premise above). Precedence is explicit CLI flag > config > default. claustrum resolves it withflag.Visit. - Default off is the zero value, and disabled bypasses the guard entirely. A
cap set to
0skips itsio.LimitReader. A timeout set to0skipscontext.WithTimeout. For the download,0instead relies onhttp.Client{Timeout: 0}, which is the stdlib's own "no timeout". Never use a huge-but-finite value. Thecap+1/ armed-cancel arithmetic is what defines the boundary, so routing the unlimited case through that arithmetic invents a boundary the reference does not have. - No opt-in bound is a hang detector. Each bound is a threshold, so an honest-but-slow or honest-but-large input trips it too. That is precisely why they are off by default.
- At the shipped defaults, no claustrum-chosen
-installwall-clock bound applies. Only stdlib transport clocks remain on the-cli-urlpath (net.Dialer{Timeout: 30s},TLSHandshakeTimeout: 10s). Those two clocks are always-on, unnumbered, and unprobed on the reference.
Catalog¶
| ID | What it does | Default | How to activate | Why (rule / clause) | Reopen trigger |
|---|---|---|---|---|---|
| D1 | SHA-256-verify the local -cli-zst blob |
trusting (no verify) | conditional — caller supplies -cli-checksum |
rule 3: only a wrong checksum pays | Desktop supplying a checksum mismatching its own SFTP blob |
| D2 | Refuse a destructive path that is or contains $HOME |
always-on | always-on | rule 3 clause (a) | an honest caller legitimately targeting a path that is/contains home |
| D3 | Cap files.extract_tar output size |
off (0 = unlimited) |
-max-extract-bytes / max-extract-bytes |
rule 4 (who-pays) | operator's cap refuses a legit extraction, or default lets a bomb through |
| D4 | files.read refuses non-regular files |
off | -files-read-regular-only / key |
rule 4 | opt-in refuses a legit read; or default parks/OOMs the daemon in normal use |
| D5 | Deadline on every git invocation |
off (0) |
-git-timeout / key |
rule 4 | opt-in kills an honest slow git |
| D6 | -cli-version must be a single path component |
always-on | always-on | rule 3 clause (b) | Desktop passing a multi-component -cli-version |
| D7 | -cli-version must not collide with the temp sweep |
always-on | always-on | rule 3 clause (b) | Desktop passing .fetch-* or *.zst |
| D8 | Decline (not share) a foreign-owned remote-server.log |
always-on | always-on | rule 3 clause (b) — unreachable on the deployed path | a shared socket dir that also needs the log file |
| D9 | Namespace-wide params binding (type error in an unread field → -32602) |
always-on | always-on | rule 3 clause (b) | a real client sending a type-mismatched unread namespace field |
| D10 | Cap -install CLI size (decompressed + download body) |
off (0) |
-max-cli-bytes / key |
rule 4 (who-pays) | Desktop ceasing to treat a disk-full message as terminal |
| D11 | Deadline on the <cli> --version runnability probe |
off (0) |
-cli-probe-timeout / key |
rule 4 | Desktop turning out not to parse cliError (retraction rider) |
| D12 | Bound on the -install download exchange |
off (0) |
-cli-download-timeout / key |
rule 4 | operator with the bound set reporting an honest slow download failed |
| D13 | Verify checksum before decompressing (-cli-url) |
always-on | always-on | UNRESOLVED — clause (c) written for it, measured not met | any change to how Desktop classifies cliError |
| D14 | Deadline on the ldd --version libc probe (linux) |
off (0) |
-libc-probe-timeout / key |
rule 4 | a musl host the glob misses whose ldd exits 0 + is slow; or the reference bounding it above 45 s |
| CT-1 | Opt-in wantPid → pid + startTime on spawn/reattach |
off (fields omitted) | caller sends "wantPid":true |
sanctioned optional-param extension | — (additive, degrades both ways) |
| CT-2 | -keep-children leaves the child tree running on shutdown |
off | -keep-children / keep-children key |
off-wire opt-in extension | — |
| CT-3 | claustrum.conf config file |
absent ⇒ stock | create the file | the opt-in mechanism itself | — |
| CT-4 | Hardened token persistence | not built (deferred idea) | — | deferred | — |
| CT-5 | -listen-pipe Windows named-pipe transport |
off | -listen-pipe / listen-pipe key (Windows) |
additive opt-in transport | — |
Tags: opt-in = operator-declinable (flag + config key). conditional = activated by the caller (D1). always-on = no switch. The CT block uses "opt-in" in the looser sense of "off unless somebody asks for it". CT-1 is caller-activated and CT-3 is the config mechanism, so neither one is operator-declinable. Only CT-2 and CT-5 carry a flag and a key.
Entries¶
D1 · Re-harden the -cli-zst checksum (conditional)¶
- Behavior. The reference verifies
-cli-checksumonly on the-cli-urldownload, not on the local-cli-zst(SFTP) blob. claustrum SHA-256-verifies-cli-zstwhen and only when the caller supplies a-cli-checksum. On a mismatch claustrum answerschecksum mismatch: …and leaves the source blob intact. An absent or empty checksum stays trusting → byte-identical to the reference. - Why conditional, not opt-in. The caller activates it by supplying
-cli-checksum(on-install, that caller is Desktop); an operator does not. It therefore has no flag or config key, and it needs none. The delta requires a wrong checksum, because a correct one is byte-identical, so no honest caller pays for it. - Observable delta (supplied-wrong checksum only): a valid blob the reference
would install returns
checksum mismatch. A corrupt blob returnschecksum mismatchinstead ofdecompressing: …. - Observed once (2026-08-10, on a download failure that the probe forced; this
is the only capture that reached the SFTP rung): the captured Desktop
-cli-zstinvocation supplied no-cli-checksum, where the-cli-urlcall seconds earlier did supply one. The condition was therefore false, and no verification ran. This is one instance of one failure shape. - Reopen trigger. Desktop supplying a
-cli-checksumthat does not match the blob it uploaded over SFTP. Conditional is not the same as unreachable. - Pointers. PROTOCOL.md →
-install;install.go.
D2 · Refuse a home directory as a destructive path target (always-on)¶
- Behavior. Two methods hand a caller-supplied,
~-expanded path toos.RemoveAll:files.extract_tarwipesdestDir, andgit.worktree_removedeletesworktreePathwhen git exits non-zero.wipesHomeDir(homeguard.go) refuses any target that is or contains the home directory. Descendants stay allowed, because extracting into~/.claude/…is the daemon's own install path. - Containment is the test, and the predicate resolves relative paths
(
filepath.Abs) before it compares them. Without that resolution,"worktreePath":".."from a daemon whose cwd is home destroys the home directory (measured —..resolves to home's parent, and the delete takes home with it).git.worktree_removeis the more exposed of the two methods:wipesHomeDiris its only gate, whileextract_taralso keepsIsAbs+isFilesystemRootbehind it. - This fired. On 2026-08-02 an in-repo fuzzer sent
"destDir":"~"at a live daemon and destroyed the maintainer's home directory."~"is the first value in the adversarial list that survives the oldIsAbs && !isFilesystemRootgate. A home directory is exactly "absolute and not a filesystem root". - Why always-on. D2 satisfies both halves of rule 3 clause (a). The reference's behaviour is unrecoverable data loss: measured, it destroys home on both methods. And no honest caller has a legitimate use for deleting home. A caller can still reach that path by accident, which is the point.
- Not a security boundary. The socket + token already grant
process.spawn(SECURITY.md). This guard stops the accidental, generated, or mistyped path. It does not resolve symlinks. - Reopen trigger. An honest caller legitimately naming a destructive target that is or contains a home directory.
- Pointers. PROTOCOL.md → both methods;
homeguard.go,homeguard_test.go(wipeDestDirseams the destructive call, so the suite is safe against an unfixed tree). Measurement: forensics.
D3 · Make the files.extract_tar size cap opt-in¶
- Behavior.
maxExtractBytescaps extraction output. An over-cap extraction returns{"success":false,"fileCount":0,"error":"extraction size limit exceeded"}and removes the truncated entry. - Default.
0= unlimited = byte-identical. Activate:-max-extract-bytes <n>or themax-extract-byteskey; disabled bypassesio.LimitReader(io.Copy(out, tr)). - Why opt-in. Measured, the reference completes a 629 MB extraction with no cap at the pin. That is a frame, not an unbounded wait, so a non-zero default fails an extraction the reference completes, and Desktop owns the argv (rule 4).
- Reopen trigger. An operator's cap refusing a legitimate extraction, or the default letting a size bomb through in normal use.
- Pointers. PROTOCOL.md;
methods_files.go. Measurement: forensics.
D4 · Make the files.read regular-file guard opt-in¶
- Behavior. With the guard on,
files.readrefuses any non-regular path with-32602 files.read: not a regular file. The reference refuses none. With the guard off, the flag short-circuits the predicate (filesReadRegularOnly && !fi.Mode().IsRegular()), so the mode check never runs. - Default. Off (byte-identical). Activate:
-files-read-regular-onlyor the key. - Why a flag and not a narrower predicate.
/dev/nulland/dev/zeroare indistinguishable by mode, so any predicate that admits the first also admits the second. - The default has two measured costs (both are the reference's own behaviour
too). First, a writerless FIFO parks a request goroutine and a descriptor:
linux reserves the fd number before it blocks, which draws down
RLIMIT_NOFILE, andaccept()shares that limit. Second, an unbounded device read (/dev/zero) never reaches EOF; under a 2 GiB cgroup cap the kernel OOM-killed both binaries.maxBytescannot prevent either cost: it keys off the stat size, which is0for every non-regular kind on linux. - Why opt-in. Across seven non-regular shapes (nine in all, with two
regular-file controls), claustrum with the guard off matches the reference
byte-for-byte. The always-on guard cost an honest
/dev/nullread a-32602that the reference never produces (rule 4). - Reopen trigger. An operator with the flag set reporting a legitimate read refused. Or, in the direction that would say the default is wrong, a report of the daemon parked or OOMed by a non-regular read in normal use.
- Pointers. PROTOCOL.md →
files.read→ Non-regular files. Full table, OOM/fd reasoning, unmeasured shapes: forensics.
D5 · Make the gitTimeout deadline opt-in¶
- Behavior. With the deadline on, claustrum bounds every git invocation (shared
gitCtxacrossgit/gitStdoutErr/gitDeadline). Ongit.worktree_removea hit answersgit worktree remove timed out after <dur>; no cleanup was attempted, and git may have partially removed the worktreeand deletes nothing. Ongit.status/git.list_branchesa hit surfaces as-32603 signal: killed. A killed repo-detection call answersisRepo:false. - Default.
0= no deadline (byte-identical). Activate:-git-timeout <dur>or the key; disabled bypassescontext.WithTimeout. - Never read a timeout as "git refused."
git.worktree_removetreats a failed git as permission to deleteworktreePath, so claustrum keeps the timeout reply separate from the failure arm. The cap is also softer than it reads:CombinedOutputwaits on git's output pipe, so a git that leaves a surviving child stays blocked past the deadline. - One opted-in arm loses data silently. If the deadline kills
copyWorktreeIncludes(worktreecopy.go) duringgit ls-files, it takes the early return, andpopulateWorktreeis best-effort. Thereforegit.worktree_createstill answers{"success":true}while every manifest-selected file is missing. No frame moves. This arm is absent at the default. - Why opt-in. The reference showed no deadline at or below 75 s on
worktree_remove; an honest 61 s git was never measured. The deadline cleared clause (a)'s not-a-frame half, but the-32603 signal: killedarm is an honest caller observing the difference (rule 4). - Reopen trigger. An operator with
-git-timeoutset reporting an honest slow git killed by it. The-32603arm makes a single report enough. - Pointers. PROTOCOL.md →
git.worktree_remove,git.list_branches;methods_git.go,worktreecopy.go.
D6 · -cli-version must name a single path component (always-on)¶
- Behavior. The install's clearing step is
os.RemoveAll(filepath.Join(cliDir, cliVersion)), so a version that reaches outside the cli-dir deletes unrelated data. claustrum answerscli version "…" must be a single path componentand touches nothing. It refuses.,.., and both/and\on every OS, so the accepted set does not change with the platform. - A single component rather than a lexical containment check. A lexical check
accepts
link/1.0.0(an intermediate symlink under the cli-dir, followed at open time), andEvalSymlinkswould only add a TOCTOU window before theRemoveAll. A final component that is itself a symlink stays legal (os.RemoveAllunlinks it rather than follows it), so the rule is narrower than "no symlinks". - Why always-on. Rule 3 clause (b). Measured, the reference destroys the
target on both
../victimandlink/1.0.0. The real client passes bare version strings (1.0.86, a commit sha,latest, all measured accepted). The evidence is an observed value plus a measured accepted-set, not an enumeration. - Reopen trigger. Desktop passing a
-cli-versionthat is not a single path component. - Pointers. PROTOCOL.md →
-install;install.go.
D7 · -cli-version must not collide with the install temp sweep (always-on)¶
- Behavior. The orphan sweep claims
.fetch-*and*.zstafter every install. Therefore-cli-version .fetch-xor1.0.zstinstalls correctly, and the sweep deletes it moments later in the same run. (Measured: both binaries finish with an empty cli-dir and nocliError, and they report success although they installed nothing.) claustrum answerscli version "…" collides with the install temp sweepinstead. The sweep predicate and this check share one definition, so they cannot drift apart. - Unlike D6 this gives up exact parity (an error beats a success that installed nothing). But that preference is not what earns the entry; clause (b) earns it.
- Why always-on. Rule 3 clause (b), on the same evidence as D6.
- Reopen trigger. Desktop passing a
-cli-versionmatching.fetch-*or*.zst(one observation reopens D6 too — both rest on the same evidence). - Pointers. PROTOCOL.md →
-install;install.go.
D8 · remote-server.log is declined rather than shared (always-on)¶
- Behavior. claustrum recreates the log fresh on every start (unlink + create, not truncate in place), which is measured parity. The divergence is only the fallback. When claustrum cannot replace the existing log — for example a sticky directory that holds another user's file — it declines the log and falls back to inherited stdio. The reference instead truncates the foreign file and writes its diagnostics into it (measured 2026-08-06).
- Hardening, not a defect claim. To reach it you need a local user who can already plant a file in that directory. The justification is a design position: a daemon should not write into a file it does not own. The justification does not depend on the reference being wrong.
- Why always-on. Rule 3 clause (b) — the trigger is unreachable on the
deployed path (
~/.claude/remote/is per-user, not world-writable). The shared directory that reaches the trigger is also the only place where the reference's behaviour is a disclosure risk. A flag would gate a branch that no honest deployment reaches. - Reopen trigger. A deployment that puts the socket directory somewhere shared and needs the log file. In that deployment the fallback sends diagnostics to the launcher's stdio, which a client may parse.
- Pointers. PROTOCOL.md → Daemon log;
server.go.
D9 · Namespace-wide params binding is stricter than the reference's (always-on)¶
- Behavior. claustrum binds
paramsinto one struct per namespace (pathParams,gitParams), so a field that is valid for the namespace but unused by this method still participates in decoding. A type-mismatched value there answers-32602(e.g.files.stat {"maxBytes":"{"},git.status {"baseRepo":[1,2]}). The reference binds only the field the method reads and ignores the rest. Both binaries ignore a genuinely unknown key. - Why always-on. Rule 3 clause (b): the trigger is a type error in a field the method does not read (a client bug). Stated honestly, this is narrower than "a real client never sends them". Desktop's per-method param set has never been enumerated against this binding, so it is unenumerated, not established.
- Reopen trigger. Any real client sending a type-mismatched value in a namespace field the target method does not read. That observation is also the measurement this entry owes and does not have.
- Pointers. PROTOCOL.md → Params presence and typing.
D10 · Make the -install CLI size cap opt-in¶
- Behavior.
maxCLIBytesgoverns two reads: the decompressed CLI (decompressing: decompressed CLI exceeds <n> bytes) and the HTTP download body (download failed: response exceeds <n> bytes). - Default.
0= unlimited (byte-identical). Activate:-max-cli-bytes <n>or the key; both call sites bypass theirLimitReaderwhen disabled. - claustrum streams the blob and never buffers it. It writes a
.blob-<random>temp (or$TMPDIR/claustrum-fetch-<random>on a first install, before the cli-dir exists) and hashes it in one pass. Therefore "cap off" does not mean unbounded memory (measured 886 MB → 10 MB on a 400 MiB payload)..blob-, not.fetch-, is load-bearing:.fetch-*is the swept namespace, so a concurrent install's sweep would delete a.fetch-blob and defeat the staging retry (errStagingVanished). The creator, both housekeeping passes, andvalidateCLIVersionall readblobTempPrefix. - Who pays for opting in. A cap set below free space replaces a disk-full report
with the cap's own message, which costs the user the free-space hint. But that
cost turns on the
cliErrordriver claim (ARCHITECTURE.md). At the default, claustrum preserves the disk-full report and matches the reference. - Why opt-in. Measured, the reference takes a 600 MiB payload to the runnability check on both the decompress and download paths. That is a frame, not an unbounded wait, so a non-zero default fails an install the reference completes, and Desktop owns the argv (rule 4).
- Reopen trigger. Desktop ceasing to treat a disk-full message as terminal
(which removes the cost). One plausible Desktop change fires this trigger and
D13's at once: Desktop broadening its terminal match to any
decompressing:error. - Pointers. PROTOCOL.md;
install.go(fetchToFile,zstdDecompress). RSS and cap-below-free-space tables: forensics.
D11 · Make the -install runnability probe deadline opt-in¶
- Behavior.
isRunnableruns<cli> --version. There are two probe sites: the cache-hit guard, and the probe after extraction. A timeout on the first site is indistinguishable from a cache miss, so an opted-in timeout produces one of three outcomes, depending on the flags. With no source it answerscli <v> missing and no --cli-url or --cli-zst provided. With a fast replacement it performs a silent reinstall, where onlycliWasPresent:falsemoves. With the same slow CLI supplied it answersinstalled cli at <path> is not runnable. On-cli-zstclaustrum consumes the blob whenever decompression succeeds. - Default.
0= no deadline (byte-identical). Activate:-cli-probe-timeout <dur>or the key; disabled bypassescontext.WithTimeout. - A bound is not a hang detector. Measured 2026-08-07: a CLI that answers honestly in 20 s makes an opted-in claustrum fail the install, delete the staged binary and consume the blob, where the reference installs it and returns at 20 s. The reference also installed a 90 s CLI, and it waited 91 s. So the reference has no deadline at or below 90 s, and any finite bound diverges for some honest input. Picking 30 s or 60 s only moves the boundary. Above 90 s is unmeasured on both.
- Why opt-in. The deadline cleared clause (a)'s not-a-frame half (the reference's wait is apparently unbounded), but an honest-but-slow CLI pays and, with Desktop owning the argv, cannot decline (rule 4). We verified the flip against the reference; we did not only argue it.
- Reopen trigger (a retraction rider, not the flip): Desktop turning out not to
parse
cliErrorafter all — see ARCHITECTURE.md. Whether any client readscliWasPresentis unprobed either way. - Pointers. PROTOCOL.md;
install.go. Probe-site table, 20 s / 90 s table, sweep/prune, zero-parsing edges: forensics.
D12 · Make the -install CLI download bound opt-in¶
- Behavior. The download once ran with
http.Client{Timeout: 5m}. It now runs withTimeout: cliDownloadTimeout, which defaults to0. The bound covers the whole exchange, including the body read, so a real body over a link that needs six minutes trips it exactly as a black hole does. - Default.
0= no bound (byte-identical) —http.Client{Timeout: 0}is the stdlib's own "no timeout" sentinel. Activate:-cli-download-timeout <dur>or the key. - Zero frees the body read, not every clock.
fetchToFileleavesTransportnil, sohttp.DefaultTransportstill appliesnet.Dialer{Timeout: 30s}andTLSHandshakeTimeout: 10son-cli-url. A SYN-black-holed host therefore fails at 30 s with the bound off. Both clocks are always-on stdlib defaults, unnumbered and unprobed on the reference. - Why opt-in. The reference showed no bound at or below 400 s on a stalled body. Measured on a valid zstd blob dribbled over ~324 s, the reference and claustrum at its default both install it, while claustrum at the retracted 5 m fails at 300 s. An honest slow download therefore pays, and Desktop owns the argv (rule 4).
- Reopen trigger. An operator with the bound set reporting an honest slow download failed by it.
- Pointers. PROTOCOL.md;
install.go(fetchToFile). Straddle and stall tables, confounders: forensics.
D13 · -install verifies the checksum before decompressing (always-on, UNRESOLVED)¶
This is the one entry that does not currently clear rule 3.
We record it rather than explain it away. It stays in the code, labelled, rather than justified by a rule bent around it.
- Behavior. On the
-cli-urlpath the reference decompresses first and aborts on the first invalid bytes. claustrum hashes the response as it streams to disk, verifies the checksum, and then decompresses. On a blob that is both undecompressable and wrong-checksummed the reference saysdecompressing: unexpected EOFwhere claustrum sayschecksum mismatch. A genuine mid-transfer interruption never reaches the checksum on claustrum, becauseio.Copy's error returns first. That case diverges on the prefix instead:download failed: <err>against the reference'sdecompressing: <err>. - Why it is unresolved. We wrote clause (c) for this entry and, measured, it
does not meet it. On both honest-path rows the reference creates an empty
cli-dir where claustrum creates nothing (when the cli-dir did not already
exist). The delta is therefore not confined to diagnostic text: an empty directory
is state a caller keeps, and a caller can distinguish it with a
files.stat. The reopen fixture run 2026-08-08 did not meet its condition. The on-disk delta is conditional on the cli-dir being absent, and it does not self-heal. - The trigger is reachable (not "an input no honest caller produces", which was measured wrong): a bad mirror, a partial upload, or a stale short proxy object is undecompressable and checksum-mismatched with no adversary. This is not the generic "flaky network" case: a genuine interruption is the prefix-divergence row above.
- Why still always-on despite being unresolved. The delta stays cheap because
neither string is disk-full-shaped. Therefore Desktop (per the
cliErrordriver claim) classifies both the same way and retries rather than fails terminally. That is a claim about a third binary, and the parity harness cannot settle it (ARCHITECTURE.md). If it is ever falsified, the cheapness argument fails and this entry owes an opt-in flip. - Reopen trigger. Any change to how Desktop classifies
cliError— e.g. distinguishingchecksum mismatchfrom a decompression error, or matching either as terminal. - Not the same as D1. D1 is about whether claustrum verifies the local
-cli-zstblob at all. D13 is about the order of verify and decompress on the-cli-urldownload. - Pointers. PROTOCOL.md → Staging and cleanup;
install.go. Ordering, clause-(c), and reopen-fixture tables: forensics.
D14 · Make the -install libc probe deadline opt-in (linux)¶
- Behavior.
detectLibcWithreturnsmuslfrom the loader glob (/lib/ld-musl-*.so.*) before it spawnsldd. Only when the glob misses does it runldd --version, and the deadline applies there. Off linux,libc_other.goreturns""without probing. Therefore the bound cannot fire on a host whose loader the glob matches. The predicate is the glob, not the host. - Default.
0= no deadline (byte-identical). Activate:-libc-probe-timeout <dur>or the key; disabled bypassescontext.WithTimeout(lddCtx). Not the same knob as-cli-probe-timeout(D11), whose name differs only in thecli/libcprefix.-cli-probe-timeoutbounds<cli> --version;-libc-probe-timeoutboundsldd --version.TestInstallArmWiresEachFlagToItsOwnGlobalexists because a swap compiles and passes every isolated test. - The value delta is narrow but not cosmetic. Fallback and true value coincide
except in one case: the glob misses and
lddreports musl andlddexits 0 (a faithful musllddexits 1) andlddis slower than the deadline. Per the driver claim that Desktop useslibcto choose a CLI build (ARCHITECTURE.md), that case means claustrum fetches a glibc build for a musl host. - Softer than it reads. The deadline fires in only one of two stall shapes: a
stalled
lddthat leaves a surviving child keeps claustrum blocked past the deadline (the same softness as D5). The deadline also addresses the stall half only. A hostilelddresolved earlier inPATHthat answers in 1 s is untouched, andclassifyLibcthen trusts itsmuslbanner verbatim. - Why opt-in. The deadline cleared clause (a)'s not-a-frame half (the reference gave no reply at 45 s in the discriminating shape), but the honest-path cost was untested in either direction, and there was no escape hatch. An untested conjunction is not a justification (rule 4).
- Reopen trigger. A musl host whose loader the glob misses and whose
lddexits 0 with a musl banner, reported together with a slowldd(all four conjuncts); or any measurement showing the reference bounds this probe above 45 s. - Pointers. PROTOCOL.md →
-install;install.go(canonical stall table),libc_linux.go,libc_other.go. Full measurement: forensics.
CT-1 · Opt-in wantPid (pid + startTime) on spawn/reattach¶
process.spawn/process.reattachaccept an optional"wantPid":true. The reply then gainspid(the child's OS pid) andstartTime. This is the first wire-surface extension; D1 by contrast changes an install-path behaviour.- Default path is byte-identical: when
wantPidis absent or false,omitemptyomits both fields, and the frame is exactly the old{"success":true}/{found,running,firstSeq,lastSeq}. The fields live on a dedicatedspawnResultstruct, so they can never leak into thesuccessResultthatprocess.stdin/process.killshare. startTimeis an opaque daemon token. It is the daemon's epoch-seconds wall clock captured at spawn, and the daemon returns it identically on spawn and reattach for the same id. Use it to detect PID reuse and orphans. It is not OS-comparable: do not equality-check it against psutilcreate_time.- The extension is tolerant in both directions: an older daemon ignores the param,
and an older client never sees the fields. A client may therefore send
wantPidunconditionally. The sibling clauster client pins the contract. - Pointers. PROTOCOL.md (
process.spawn+process.reattach);results.go.
CT-2 · Opt-in -keep-children serve flag¶
- A
-serveflag, off the wire: it changes no method, no frame and no capability. At the default (off), graceful shutdown kills the whole child tree. Set, it leaves spawned children running so they survive a daemon restart/upgrade, and it logs one line with the surviving count. The new daemon does not re-adopt them; an out-of-band consumer reconciles them through the CT-1pid/startTime. - Caveat: survivors lose their stdio. The daemon-side pipe ends die with the daemon: the child sees EOF on stdin, and a later write gets SIGPIPE/EPIPE. Therefore only children that tolerate dead stdio genuinely survive.
- POSIX-only. On Windows a Job Object confines the children, and the OS
terminates that Job Object on daemon exit in any case. claustrum therefore ignores
the flag and prints a startup warning (
honorKeepChildren). - Activate:
-keep-childrenor thekeep-childrenkey. - Pointers. PROTOCOL.md (
-serveflags).
CT-3 · Opt-in claustrum.conf config file¶
- A single place to turn deviations on; absent ⇒ stock. This is an optional
key = valuefile, read from the directory that holds the binary. If the file is missing, unreadable, non-regular, or malformed, claustrum behaves as a stock replica. Every key gates an already-opt-in divergence. The file adds zero new dependency (stdlibbufio+strings.Cut,#comments). claustrum ignores unknown keys and invalid values, which keeps the format forward-compatible and fail-safe. Precedence: explicit CLI flag > config > default. - Keys mirror the flags:
version-override,keep-children,metrics-addr,wire-log,wire-log-max-string,listen-pipe,max-extract-bytes(D3),max-cli-bytes(D10),cli-probe-timeout(D11),cli-download-timeout(D12),libc-probe-timeout(D14),git-timeout(D5),files-read-regular-only(D4). Durations usetime.ParseDuration, which rejects a bare number, except zero; zero parses in unboundedly many spellings and always means disabled. No accepted oddity can switch a divergence on. version-overridemakes claustrum a permanent drop-in. The desktop client decides whether to re-upload the daemon: it runs<bin> --versionon the cached~/.claude/remote/srv/<pinned-sha>/serverand matches the output against the SHA it pins. Stock claustrum prints its own version, so the client re-SFTPs the reference every session. Set the key to that bare commit SHA (40-hex git SHA-1; 64-hex also accepted; anything else is a no-op). claustrum then printsclaude-ssh <sha> (via Claustrum …), the client hits the cache, and it stops overwriting.- Off the wire, off by default.
-versionis CLI stdout, not a JSON-RPC frame;server.version/server.capabilitiesstill report claustrum's own version. - Fail-safe & hardened: regular-file-only via
Lstat,io.LimitReader≤ 64 KiB, per-key validation (version-overridegated to^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$and lower-cased). claustrum uses every value as data, never as a format string. Any doubt → stock; startup never fails. - Pointers. PROTOCOL.md (
-version);config.go.
CT-4 · Opt-in hardened token persistence — deferred idea¶
- Context. The daemon persists its token to
daemon.token(0600) beside the socket, so a client can reconnect. That is parity, and it is on by default (tokenpersist.go). There are two accepted parity caveats. The file survives an unclean kill or crash, because cleanup runs only on graceful shutdown. And on Windows,0600is not an owner-only DACL. - Idea (not built). A
claustrum.confkey — e.g.persist-token = false— and/or a Windows owner-only DACL on the file, for operators who prefer a smaller on-disk token window over drop-in reconnect. Must stay absent ⇒ stock. - Why deferred. There is no demand. The default matches the reference, and the socket directory is already owner-scoped in the real deployment. We record the idea so the security trade-off is not lost.
- Pointers. PROTOCOL.md → Token persistence;
tokenpersist.go.
CT-5 · Opt-in -listen-pipe Windows named-pipe transport¶
- Shipped.
-listen-pipemakes-serveadditionally serve the exact same NDJSON JSON-RPC dispatch over a Windows named pipe, concurrently with theAF_UNIXsocket. Off ⇒ stock. The wire contract, the field ordering and the framing stay the same whether a request arrives over the socket or over the pipe. - Why. Without the pipe, a Windows client that cannot consume an
AF_UNIXsocket cannot attach. Pythonasynciois the notable example: its Windows Proactor loop natively supports named pipes. This was clauster's ask. - Discovery + auth. claustrum picks the pipe name
(
\\.\pipe\claustrum-<instance-id>, client-opaque) and publishes it torpc.pipebeside the socket (atomic write before accepting; claustrum removes it on graceful shutdown). Same in-band"auth"+daemon.tokenhandshake. Owner-only DACL (SDDLD:P(A;;GA;;;<current-user-SID>)), local-only, no new authenticated surface (SECURITY.md). - Windows-only: elsewhere claustrum ignores the flag and prints a warning
(
honorListenPipe). A setup failure is non-fatal: the socket still serves. - Activate:
-listen-pipeor thelisten-pipekey. - Pointers. PROTOCOL.md (
-serveflags);pipetransport.go,pipetransport_windows.go.
Candidates considered but not taken¶
In both places claustrum was friendlier than the reference, and matching the reference would cost something real. We record them so the code can point somewhere durable. No decision is implied, and nothing here is shipped or scheduled.
- Conditional
-stopsocket unlink.-stopremoves the socket path on every exit, including when no daemon answered. That matches the reference (measured on three arms, two of them attributing: the live-daemon control says nothing, because the daemon removes the socket itself). So-stopremoves a path that it did not create and whose owner it cannot identify.os.Removedoes not distinguish shapes, so a regular file or an empty directory at the-socketpath goes the same way. Astat-first variant that removed only a socket would be strictly safer and a divergence. - Fail fast on a missing
-servetoken source. The check runs in the detached child, so the launcher reports its ~10 s accept timeout, and the real reason reaches only the child's log. That is reference parity (measured 10.02 s vs 10.07 s). A parent-side check answered in 0.03 s and named the actual problem: a better operator experience, and a divergence.
Explicitly out of scope (would break compatibility)¶
- Changing method names, params, result field order, error codes, or the stream-frame shape.
- Replacing the in-band
"auth"scheme. - Adding required new params to existing methods. (The sanctioned exception is an optional, gracefully-ignored param whose result fields vanish by default — the D1 / CT-1 pattern. It leaves the default frame byte-identical, and it degrades both ways.)
Any of these would need a deliberate, documented protocol version bump.