You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CHANGELOG.md
+9-1Lines changed: 9 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,15 @@
4
4
5
5
### Fixed
6
6
7
-
- **`--remote-control` no longer clobbers another component's `NODE_EXTRA_CA_CERTS`.** That variable takes exactly one file, so on a host where something else also MITMs `api.anthropic.com` (a corporate agent, an account-pinning proxy) the last writer won and every other CA was silently untrusted — measured breaking Remote Control inbound. The launcher now publishes its own CA to `${CLAUDE_CONFIG_DIR:-~/.claude}/ca-trust.d/ccf.pem` (own filename only, never a sibling's, rewritten every launch, atomically via temp + `rename`) and reads a merged `ca-trust.pem` if one exists. It never writes the merged bundle: merging needs ambient corporate-root discovery, which is environment-specific and belongs outside this repo. The bundle is used only when every PEM block in it parses **and** one of them is our own CA (compared by DER) — a bundle that is torn or predates our publish is worse than none, since it makes the client distrust the very proxy it is routed through. On a host with no other MITM and no bundle, behavior is byte-identical to before. Both paths are fixed names under the config dir with no env override: they are two halves of one rendezvous, so a knob on either half alone would let a participant drop out of the contract while appearing to implement it. See [Coexisting with another MITM](README.md#coexisting-with-another-mitm-on-the-same-machine-ca-trustd).
7
+
- **`--remote-control` no longer clobbers another component's `NODE_EXTRA_CA_CERTS`.** That variable takes exactly one file, so on a host where something else also MITMs `api.anthropic.com` (a corporate agent, an account-pinning proxy) the last writer won and every other CA was silently untrusted — measured breaking Remote Control inbound. The launcher now publishes its own CA to `${CLAUDE_CONFIG_DIR:-~/.claude}/ca-trust.d/ccf.pem` (own filename only, never a sibling's, rewritten every launch, atomically via temp + `rename`) and reads a merged `ca-trust.pem` if one exists. It never writes the merged bundle: merging needs ambient corporate-root discovery, which is environment-specific and belongs outside this repo. The bundle is used only when every PEM block in it is terminated **and** one of its `CERTIFICATE` blocks is our own CA (compared by DER) — a bundle that is torn or predates our publish is worse than none, since it makes the client distrust the very proxy it is routed through. On a host with no other MITM and no bundle, behavior is byte-identical to before. Both paths are fixed names under the config dir with no env override: they are two halves of one rendezvous, so a knob on either half alone would let a participant drop out of the contract while appearing to implement it. See [Coexisting with another MITM](README.md#coexisting-with-another-mitm-on-the-same-machine-ca-trustd).
8
+
9
+
- **The `ca-trust.pem` guard now matches what Node's CA loader actually accepts.** As shipped it disagreed with a real handshake on 8 of 20 measured bundle shapes (node v24.11.1 / openssl 3.6.1). Seven were needless refusals of healthy bundles — any non-certificate block (a CRL, a public key, key material), or a provenance comment that merely mentioned `-----BEGIN `, voided the whole file — and refusing is not the safe direction: it silently drops every sibling and corporate CA for that session, which is the failure this contract exists to prevent. The eighth was the dangerous direction: our own CA relabelled `TRUSTED CERTIFICATE` parses to byte-identical DER, so the guard reported "carries our CA" while Node's loader skipped the block entirely and every request failed TLS. The guard now anchors markers to line starts (tolerating trailing whitespace, which openssl still reacts to), bounds each block's `END` search at the next `BEGIN` so a torn entry cannot borrow a later block's terminator, requires every block to decode — X509 for `CERTIFICATE`, valid base64 armor for everything else, since Node aborts the whole load on any block it cannot read regardless of label — and requires the DER match to land on a `CERTIFICATE` block. Where it cannot tell (a block damaged *after* ours, whose truncated body may still decode) it refuses — conservative, never permissive. The decision moved to `bin/ca-trust.mjs` so the tests drive the shipped code: it was previously inline in a top-level script with a hand-copied twin in the test file, and mutating the real one left the entire suite green.
10
+
11
+
-**Orphaned publish temps are reaped even when publishing fails.** The reaper shared the publish `try`, so `rename` throwing skipped it — meaning that on exactly the hosts where publishing is persistently broken (a root-owned `ccf.pem`, a read-only mount, `ENOSPC`) each launch abandoned one full-CA temp and collected none, growing without bound in the directory a bundle builder globs.
12
+
13
+
-**A corrupt `ca.pem` no longer blames the merged bundle, and is no longer handed to the client.** Parsing our own CA sat inside the bundle `try`, so an unparseable `ca.pem` was reported as `ignoring <ca-trust.pem> (...)` — naming a file that may be perfectly healthy — and then `NODE_EXTRA_CA_CERTS` was pointed at the very file that had just failed to parse. It is now parsed in its own step and named in its own message, and when it does not parse the variable is left unset: node falls back to its built-in store, which is the honest state, rather than to a file we vouch for and cannot read.
14
+
15
+
-**The launcher no longer prints the wiring recipe that would undo its own coexistence.** The spawned proxy's `export NODE_EXTRA_CA_CERTS=<our ca.pem>` banner is correct standalone advice and exactly wrong under `--remote-control`, which relays that stderr: it appeared right after the launcher had published to `ca-trust.d` and adopted the merged bundle, telling the operator to pin the variable to our CA alone. The server now prints the recipe only when the operator is the one wiring: `process.channel` is set exactly when our launcher `fork()`ed it, and the launcher is the only `fork()` site (the `server` subcommand uses `spawn`, and a service manager runs it bare). The mode line still prints either way, so forward-proxy mode stays visible. Standalone, the recipe carries a same-host-MITM caveat, as do the README's manual-wiring recipes.
0 commit comments