Skip to content

Commit c8acf51

Browse files
codeslakeclaude
andcommitted
refactor(launcher): use process.channel instead of filtering relayed stderr
Two simplifications, both measured before applying. The launcher was line-buffering the proxy's stderr and stripping the wiring recipe with a regex — 20 lines to remove text it had caused. The server can tell directly: process.channel is set exactly when fork() created the process, and the launcher is the only fork() site (the `server` subcommand uses spawn, and a service manager runs it bare). Measured: fork -> channel set, standalone -> undefined. So the recipe is now gated at the source and the relay is a plain pass-through again. And bundleCarriesOurCA no longer normalizes CRLF. `$` in a /m regex matches before a `\r`, and the END search is anchored on the leading `\n`, so both halves already read a CRLF file the same as an LF one. Measured across 102 shapes (34 bundle layouts x LF/CRLF/mixed): identical verdicts with and without the replace, and 0 false accepts against a real handshake. No behavior change: standalone still prints the full recipe, the launcher still prints the mode line and not the recipe, and the guard's verdicts are unchanged. Mutating the new gate to false fails exactly one test, and all five guard-clause mutations are still caught. -16 net production lines. Full suite 1503 pass / 0 fail. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8f46b89 commit c8acf51

3 files changed

Lines changed: 21 additions & 37 deletions

File tree

bin/ca-trust.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ import { X509Certificate } from "node:crypto";
1818
//
1919
// It stays a pre-flight guard, never proof: it establishes the file parses and
2020
// carries us, never that node will verify a given leaf with it.
21-
export function bundleCarriesOurCA(merged, ourCaPem) {
22-
const text = merged.replace(/\r\n/g, "\n");
21+
export function bundleCarriesOurCA(text, ourCaPem) {
2322
const ourDer = new X509Certificate(ourCaPem).raw;
2423
// Line-anchored. openssl only honours a marker that begins a line, so a
2524
// marker quoted inside prose is not a block. The previous count-based check
2625
// read the raw file and rejected any bundle whose provenance header happened
2726
// to name the marker — measured: `# see -----BEGIN CERTIFICATE-----` ahead of
2827
// a healthy CA was refused while node authorized the same bytes.
28+
//
29+
// No CRLF normalization: `$` in a /m regex matches before a `\r`, and the END
30+
// search below is anchored on the leading `\n`, so both halves already read a
31+
// CRLF file the same as an LF one. Measured across 102 shapes (34 bundle
32+
// layouts x LF/CRLF/mixed): identical verdicts with and without the replace.
2933
const marker = /^-----BEGIN ([A-Z0-9 ]*)-----$/gm;
3034
let carriesUs = false;
3135
for (let m; (m = marker.exec(text)); ) {

bin/claude-via-proxy.mjs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -158,28 +158,8 @@ proxyProc.on("exit", (code) => {
158158
}
159159
});
160160

161-
// The proxy's "wire the client" recipe is correct standalone advice and exactly
162-
// wrong relayed from here: we have already wired claude via ca-trust.d and the
163-
// merged bundle, so an `export NODE_EXTRA_CA_CERTS=<our ca.pem>` printed right
164-
// after that work tells the operator to undo it — the variable takes one file,
165-
// so pinning it to our CA alone silently untrusts every other MITM on the host.
166-
// Dropped here rather than suppressed at the source: the launcher is the one
167-
// with the context, and a new env var to carry it would be permanent surface
168-
// area for a presentation detail. Only the recipe goes; the mode line stays, so
169-
// forward-proxy mode is still visible in the output.
170-
const WIRING_RECIPE = /^\s*(export (HTTPS_PROXY|NODE_EXTRA_CA_CERTS)=|\(if anything else on this host|\s*`claude-via-proxy --remote-control`|\s*one file, so setting it here)/;
171-
let stderrTail = "";
172161
proxyProc.stderr.on("data", (chunk) => {
173-
const text = stderrTail + chunk.toString();
174-
// Keep a trailing partial line for the next chunk so a recipe line split
175-
// across two reads is still matched whole.
176-
const nl = text.lastIndexOf("\n");
177-
stderrTail = nl === -1 ? text : text.slice(nl + 1);
178-
const complete = nl === -1 ? "" : text.slice(0, nl + 1);
179-
if (!complete) return;
180-
const kept = complete.split("\n").filter((l, i, a) =>
181-
!(i === a.length - 1 && l === "") && !WIRING_RECIPE.test(l));
182-
if (kept.length) process.stderr.write(kept.join("\n") + "\n");
162+
process.stderr.write(chunk);
183163
});
184164

185165
function waitForReady() {

proxy/server.mjs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -614,20 +614,20 @@ export async function startProxy(options = {}) {
614614

615615
const addr = server.address();
616616
if (forwardProxyCA) {
617-
// The caveat is part of the recipe, not a footnote: NODE_EXTRA_CA_CERTS
618-
// takes exactly one file, so an operator who follows these two lines on a
619-
// host where something else also MITMs api.anthropic.com silently untrusts
620-
// that component. --remote-control exists precisely to avoid that, and the
621-
// launcher filters this recipe out of its relayed stderr since it has
622-
// already wired the client via ca-trust.d.
623-
process.stderr.write(
624-
"[cache-fix] forward-proxy: on. Wire the client (leave ANTHROPIC_BASE_URL UNSET so Remote Control stays enabled):\n" +
625-
` export HTTPS_PROXY=http://${addr.address}:${addr.port}\n` +
626-
` export NODE_EXTRA_CA_CERTS=${forwardProxyCA}\n` +
627-
" (if anything else on this host also MITMs api.anthropic.com, use\n" +
628-
" `claude-via-proxy --remote-control` instead — that variable takes\n" +
629-
" one file, so setting it here would untrust the other CA)\n",
630-
);
617+
const wiredByLauncher = process.channel !== undefined;
618+
// Only when the operator is the one wiring. process.channel is set exactly
619+
// when our launcher fork()ed us, and it has already wired claude via
620+
// ca-trust.d — printing `export NODE_EXTRA_CA_CERTS=<our ca.pem>` into its
621+
// relayed stderr tells the operator to undo that: the variable takes one
622+
// file, so pinning it to our CA alone untrusts every other MITM on the host.
623+
process.stderr.write(wiredByLauncher
624+
? "[cache-fix] forward-proxy: on. Client wired by the launcher (ca-trust.d).\n"
625+
: "[cache-fix] forward-proxy: on. Wire the client (leave ANTHROPIC_BASE_URL UNSET so Remote Control stays enabled):\n" +
626+
` export HTTPS_PROXY=http://${addr.address}:${addr.port}\n` +
627+
` export NODE_EXTRA_CA_CERTS=${forwardProxyCA}\n` +
628+
" (if anything else on this host also MITMs api.anthropic.com, use\n" +
629+
" `claude-via-proxy --remote-control` instead — that variable takes\n" +
630+
" one file, so setting it here would untrust the other CA)\n");
631631
}
632632
let closed = false;
633633
return {

0 commit comments

Comments
 (0)