Skip to content

fix: round five — a CLI harness quoting its own key - #218

Merged
fstubner merged 1 commit into
mainfrom
fix/cli-harness-key-leak
Sep 4, 2026
Merged

fix: round five — a CLI harness quoting its own key#218
fstubner merged 1 commit into
mainfrom
fix/cli-harness-key-leak

Conversation

@fstubner

@fstubner fstubner commented Sep 4, 2026

Copy link
Copy Markdown
Owner

The previous sweep flagged this as a gap it could not reproduce without a real harness. It reproduces fine with a stub one, and it is a real leak.

The finding

A CLI route is handed its credential in an environment variable, and a harness reporting an auth failure can print it back. Nothing removed it.

Measured against the built binary, before:

dispatch: leaky_cli (explicit)
auth error: rejected key FAKECLIKEY_abcdef123456
logs/dispatches.jsonl -> error: auth error: rejected key FAKECLIKEY_abcdef123456

After:

auth error: rejected key <redacted>
logs/dispatches.jsonl -> leaked: no

How narrow it is

Reaching it needs api_key: on a CLI route, which reclassifies the route as metered — billing policy then blocks it until the user explicitly opts in. I hit that block first and had to opt in to reproduce, so it is genuinely narrower than the endpoint leaks beside it. Once opted in it is the same disclosure. Three shipped harnesses (claude_code, codex, cursor) declare an api_key_env_var, and setting a key on them is documented.

Why a wrapper and not five scrub calls

This class has now been found five times, and four of those were the fix landed in one branch and the sibling beside it kept leaking — twice inside a single file. #runStream has five result sites plus the chunk events, and a sixth added later would silently miss a per-site scrub. It cannot miss the wrapper.

Chunks are scrubbed too — they become partialOutput and stdout.log on disk, the same disclosure a beat earlier. A route with no key (every subscription CLI) takes an early yield* and pays nothing.

Sabotage

Bypassing the wrapper fails exactly the two scrub tests. The third test — a no-key route's output arriving byte-identical — keeps passing, which is what says the common path is unaltered rather than merely untested.

npm run check: 1231 passed, 8 skipped, exit 0.

The previous sweep flagged this as a gap it could not reproduce without a
real harness. It reproduces fine with a stub one, and it is a real leak.

A CLI route is handed its credential in an environment variable, and a
harness reporting an auth failure can print it back. Nothing removed it. A
stub harness writing `auth error: rejected key <key>` to stderr produced
that string verbatim in the terminal and in logs/dispatches.jsonl, measured
against the built binary before and after.

Narrower than the endpoint leaks beside it: `api_key:` on a CLI route
reclassifies it as metered, which billing policy blocks until the user
explicitly opts in. Confirmed by hitting that block first. Once opted in it
is the same disclosure.

The scrub wraps the dispatcher's whole event stream rather than each result
site, and that is the entire point. This class has now been found five
times and four were "the fix landed in one branch, the sibling beside it
kept leaking" — twice inside a single file. #runStream has five result
sites plus the chunk events; a sixth added later would silently miss a
per-site scrub. It cannot miss the wrapper.

Chunks are scrubbed too: they become partialOutput and stdout.log on disk,
which is the same disclosure a beat earlier. A route with no key — every
subscription CLI — takes the untouched path and pays nothing.

Sabotage: bypassing the wrapper fails exactly the two scrub tests, while
the no-key test keeps passing, which is what says the common path is
unaltered.

npm run check: 1231 passed, 8 skipped, exit 0.
@fstubner
fstubner merged commit fa0f71a into main Sep 4, 2026
4 checks passed
@fstubner
fstubner deleted the fix/cli-harness-key-leak branch September 4, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant