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
fix(launcher): age-gate the temp reaper; drop the publish-dir override
Two contract-level fixes from review of this PR.
The reaper deleted every ccf.pem.* after a publish, which cannot distinguish
an orphan from a CONCURRENT launcher's temp — one that has been written but
not yet renamed. Deleting that makes the peer's renameSync throw a publish
failure we caused, and leaves whichever launcher won first on disk rather
than the current publisher's bytes. Name carries no provenance, so age is the
signal: the write-to-rename window is one small write to the same directory,
microseconds, and a minute of gate is four orders of magnitude of headroom.
Reaping late costs nothing (nothing reads these); reaping early breaks a peer.
CACHE_FIX_CA_TRUST_DIR is removed. The publish path had an override while the
read path was a fixed name, so setting it made this launcher publish where no
builder looks while still consuming the canonical bundle — silently dropping
out of the contract while appearing to implement it. The two paths are halves
of one rendezvous and must move together; CLAUDE_CONFIG_DIR already does that.
The new test asserts both reaper outcomes across ONE launch: an old orphan is
collected and a fresh sibling survives. Mutation-checked in both directions —
removing the age gate reddens the survives assertion, removing the rm reddens
the collected one.
README now also states the boundary explicitly: the guard proves parses +
carries us, never contains only approved writers. This is a cooperative
convention among same-user processes, not a defense against a local attacker,
who could equally replace ccf.pem or the CA dir.
Co-Authored-By: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@
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. New opt-in env var: `CACHE_FIX_CA_TRUST_DIR`. 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 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).
Copy file name to clipboardExpand all lines: README.md
+15-1Lines changed: 15 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -132,12 +132,27 @@ that case, and when no bundle exists at all, the launcher falls back to our own
132
132
CA and behaves exactly as it did before any of this existed. **A host with no
133
133
other MITM and no bundle builder sees no change.**
134
134
135
+
Both paths are fixed names under `<config>`, deliberately with no env override
136
+
of their own. They are two halves of one rendezvous: a knob on either half alone
137
+
lets a participant publish where no builder looks, or read a file no builder
138
+
writes, while still appearing to implement the contract. `CLAUDE_CONFIG_DIR`
139
+
already relocates the pair, and it moves both halves together.
140
+
135
141
Note the limit of what a consumer can check: intact, and carries my CA. Whether
136
142
the bundle is *complete* — that no corporate root went missing — is the
137
143
builder's guarantee, not something a reader can verify, because a reader has no
138
144
previous state to compare against and a legitimately small bundle is
139
145
indistinguishable from a narrowed one.
140
146
147
+
**This is a cooperative convention among same-user processes, not a trust
148
+
boundary.** The check proves *parses, and carries us* — never *contains only
149
+
approved writers*. Anyone who can write `<config>` can hand us a well-formed
150
+
bundle holding our CA plus their own and it will be accepted, exactly as they
151
+
could already have replaced `ca-trust.d/ccf.pem`, the CA dir, or this file. The
152
+
contract defends against components accidentally untrusting each other, which is
153
+
the failure that actually happens; it does not defend against a local attacker,
154
+
who has simpler routes.
155
+
141
156
#### `CACHE_FIX_DOWNLOAD_REWRITE` breaks `claude update` — leave it off
142
157
143
158
`CACHE_FIX_DOWNLOAD_REWRITE=on` reads like a pure performance knob. It is not:
@@ -311,7 +326,6 @@ All proxy settings are controlled via environment variables. Set them before sta
311
326
|`CACHE_FIX_PROXY_UPSTREAM`|`https://api.anthropic.com`| Upstream URL. Change to chain another proxy (e.g. `http://localhost:8080`) |
312
327
|`CACHE_FIX_FORWARD_PROXY`| unset | Set to `on` for forward-proxy mode (HTTP CONNECT + selective MITM of the upstream host) so the client points `HTTPS_PROXY` at the proxy instead of `ANTHROPIC_BASE_URL`, keeping Remote Control enabled. See [Forward-proxy mode](#forward-proxy-mode-keeps-remote-control-working). |
313
328
|`CACHE_FIX_CA_DIR`|`~/.claude/cache-fix-ca`| Directory for the forward-proxy CA/leaf cert (generated once on first start). The client trusts `ca.pem` via `NODE_EXTRA_CA_CERTS`. |
314
-
|`CACHE_FIX_CA_TRUST_DIR`|`$CLAUDE_CONFIG_DIR/ca-trust.d`| Where `--remote-control` publishes our CA as `ccf.pem` so another MITM on the same host can merge it. See [Coexisting with another MITM](#coexisting-with-another-mitm-on-the-same-machine-ca-trustd). |
315
329
|`CACHE_FIX_PROXY_TIMEOUT`|`600000`| Request timeout in milliseconds |
316
330
|`CACHE_FIX_EXTENSIONS_DIR`|`proxy/extensions/`| Directory for extension `.mjs` files |
0 commit comments