Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ picker lists Anthropic's own models until the removal succeeds. OpenCodex rememb
still needs removal and retries on the next restart; `ocx claude desktop picker status` shows the
picker as unavailable meanwhile.

Picker mode allows up to 64 KiB of headers on incoming requests and ordinary HTTP
responses, preserving browser session cookies. Larger upstream response headers return
502 and log `upstream:headers-too-large`, without cookie values or request paths.
Upgraded connections continue to relay bytes directly after the request handshake.

While picker mode is on, Claude Desktop reaches the network through OpenCodex. If OpenCodex stops,
Desktop is offline until you fully restart it or turn picker mode off. Check the state with
`ocx claude desktop picker status`; use `ocx claude desktop picker trust` to repeat the trust step,
Expand Down
15 changes: 12 additions & 3 deletions src/claude/intercept/picker-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export interface PickerListenerOptions {
}
export interface PickerListenerHandle { port: number; close(): Promise<void> }

// Browser session cookies can exceed the HTTP compatibility layer's 16 KiB default.
// Keep both sides bounded, while allowing ordinary desktop session headers through.
export const PICKER_MAX_HEADER_BYTES = 64 * 1024;

const HOP_HEADERS = new Set([
"connection", "keep-alive", "proxy-connection", "proxy-authenticate", "proxy-authorization",
"te", "trailer", "transfer-encoding", "upgrade",
Expand Down Expand Up @@ -65,7 +69,7 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
const upstream = options.upstream ?? { host: "claude.ai", port: 443, servername: "claude.ai" };
const cap = options.maxEncodedBytes ?? BOOTSTRAP_MAX_ENCODED_BYTES;
const upgrades = new Set<Duplex>();
const server = createServer({ cert: options.leaf.certPem, key: options.leaf.keyPem, ALPNProtocols: ["http/1.1"] });
const server = createServer({ cert: options.leaf.certPem, key: options.leaf.keyPem, ALPNProtocols: ["http/1.1"], maxHeaderSize: PICKER_MAX_HEADER_BYTES });

server.on("request", (req: IncomingMessage, res: ServerResponse) => {
const method = req.method ?? "GET";
Expand All @@ -87,7 +91,7 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
const upReq = httpsRequest({
host: upstream.host, port: upstream.port, servername: upstream.servername,
ca: upstream.ca, rejectUnauthorized: true, agent: false,
method, path: req.url, headers,
method, path: req.url, headers, maxHeaderSize: PICKER_MAX_HEADER_BYTES,
}, upRes => {
const status = upRes.statusCode ?? 502;
const originalHeaders = filteredHeaders(upRes.rawHeaders);
Expand Down Expand Up @@ -132,7 +136,12 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
res.end(rewritten ?? original);
});
});
upReq.on("error", fail);
upReq.on("error", error => {
if ((error as NodeJS.ErrnoException).code === "HPE_HEADER_OVERFLOW") {
options.log?.(`picker ${method} ${category} upstream:headers-too-large`);
}
fail();
});
req.on("error", () => upReq.destroy());
res.on("close", () => { if (!res.writableEnded) upReq.destroy(); });
req.pipe(upReq);
Expand Down
12 changes: 9 additions & 3 deletions structure/clients/claude-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ The User-Agent is a routing hint, not a trust boundary: a client that fakes it r
any local process already reaches (the `api.anthropic.com` intercept is on the Claude Code proxy
too; the `claude.ai` relay verifies upstream and adds no credential) and breaks only its own TLS,
because each terminator presents a certificate only its intended client trusts. `claude.ai:443` is
terminated by a `node:https` HTTP/1.1 relay (`picker-listener.ts`) only while the runtime's cached
terminated by a `node:https` HTTP/1.1 relay (`picker-listener.ts`) with a bounded 64 KiB
incoming-request and ordinary upstream-response header allowance for browser session cookies,
only while the runtime's cached
decision is armed: macOS, persisted resolved Desktop mode first-party, Desktop intent on,
`claudeCode.intercept.picker !== false`, no disarm latch, listener up, and the current picker CA
trusted in the login keychain (`picker-trust.ts`). The picker CA (`picker-ca.ts`) carries critical
Expand Down Expand Up @@ -242,8 +244,12 @@ remote `ccr` (`picker-bootstrap.ts`), failing open to the original bytes; the mo
comes from a persisted snapshot (`picker-models.ts`), so a bootstrap never waits on discovery. Picker aliases carry `[1m]` only for authoritative windows of at least 1M, using the shared context marker helper with auto-context disabled. Sub-million opt-ins remain unmarked because the picker cannot guarantee the Desktop runner's compaction environment. A
CONNECT to claude.ai that arrives before the first refresh waits at most 3 s, then goes blind. A
picker proxy bind failure only disables picker mode; a picker construction or start failure closes
every socket the start had bound before rethrowing. Nothing is logged but method, bootstrap or
other, and status.
every socket the start had bound before rethrowing. Ordinary session cookies within the header
allowance relay unchanged. Upstream header overflow returns an empty 502 and logs the fixed
reason `upstream:headers-too-large`; other records contain only method, bootstrap or other,
status, and fixed bootstrap rewrite outcomes. Header values and request paths are not logged.
Upgraded connections retain raw TLS relay semantics; their upstream bytes do not pass through
the ordinary HTTP response parser.

### Picker catalog rewrite bounds

Expand Down
Loading
Loading