Process argv is a credential channel we never enumerated: 4 secrets are readable by any local user via ps #190

Closed
opened 2026-08-31 03:41:01 +02:00 by ltms · 1 comment
Owner

Found on the Mac on 2026-08-31, while checking whether a delegated worker was making progress. I ran
pgrep -fl and it printed two live credentials straight into my transcript.

What is exposed

Seven chrome-devtools-mcp processes are running with their whole environment passed as part of
their argv
, not as a real process environment. Extracting names only (never values), that argv
carries 15 environment variable names, 4 of them credential-shaped:

AI_GATEWAY_TOKEN
AWS_ACCESS_KEY_ID
CLAUDE_CODE_MESSAGING_TOKEN
N8N_ENCRYPTION_KEY

argv is world-readable. Any local user can run ps -axww and read all four in full. This is
unlike the real process environment, which macOS only shows to the owning user. All the work we have
done on the member environment — the CB-596 deny list, the CB-633 ZDOTDIR scrub, sshAuthSock —
protects a child process's environment. None of it touches argv, and argv is the weaker of the two.

AWS_ACCESS_KEY_ID is the AdministratorAccess key already noted in the CB-633 work.

What is NOT exposed

I checked, names only:

process credential-shaped names in argv
fleetd.jar 0
herdr 0
claude (members and lead) 0

So fleetd is not the source. It sends {cwd, env, argv} to herdr as separate fields, and herdr
sets the environment properly. This comes from how chrome-devtools-mcp is launched in the
operator's Claude Code MCP config, which appends NAME=value pairs to the command line.

Why file it here anyway

Because of the pattern, which this repo has now hit three times:

We enumerate one channel and then conclude about all of them.

secrets.sh was audited, and the environment was missed (SSH_AUTH_SOCK). The environment was
audited, and the filesystem was missed (#184: the forge key is a readable passphrase-free file). Now
the environment has been audited again, and argv was missed. Each time the conclusion was
"members are contained", and each time it was true only of the one channel that was looked at.

Actions

  1. Rotate all four. AI_GATEWAY_TOKEN is already on #159. AWS_ACCESS_KEY_ID is the
    AdministratorAccess key. Add CLAUDE_CODE_MESSAGING_TOKEN and N8N_ENCRYPTION_KEY.
  2. Fix the launch config so chrome-devtools-mcp receives its environment as an environment,
    not as argv. This is operator config, outside this repo.
  3. Add argv to the threat model in whatever we write about member containment, and say plainly
    that argv beats every environment control we have.

A rule for agents working in this repo

ps, pgrep -fl and ps -f print argv, and argv can hold secrets. Add them to the list next to
git remote -v and ${VAR:-x}. To inspect processes, extract names and never values:

pgrep -fl <pattern> | grep -oE '[A-Z][A-Z0-9_]{2,}=' | sed 's/=$//' | sort -u

grep -o on a pattern ending at = cannot emit the value, so this is safe by construction.

Found on the Mac on 2026-08-31, while checking whether a delegated worker was making progress. I ran `pgrep -fl` and it printed two live credentials straight into my transcript. ## What is exposed Seven `chrome-devtools-mcp` processes are running with their whole environment passed **as part of their argv**, not as a real process environment. Extracting names only (never values), that argv carries **15 environment variable names, 4 of them credential-shaped**: ``` AI_GATEWAY_TOKEN AWS_ACCESS_KEY_ID CLAUDE_CODE_MESSAGING_TOKEN N8N_ENCRYPTION_KEY ``` **argv is world-readable.** Any local user can run `ps -axww` and read all four in full. This is unlike the real process environment, which macOS only shows to the owning user. All the work we have done on the member environment — the CB-596 deny list, the CB-633 ZDOTDIR scrub, `sshAuthSock` — protects a *child process's environment*. None of it touches argv, and argv is the weaker of the two. `AWS_ACCESS_KEY_ID` is the AdministratorAccess key already noted in the CB-633 work. ## What is NOT exposed I checked, names only: | process | credential-shaped names in argv | |---|---| | `fleetd.jar` | 0 | | `herdr` | 0 | | `claude` (members and lead) | 0 | So **fleetd is not the source**. It sends `{cwd, env, argv}` to herdr as separate fields, and herdr sets the environment properly. This comes from how `chrome-devtools-mcp` is launched in the operator's Claude Code MCP config, which appends `NAME=value` pairs to the command line. ## Why file it here anyway Because of the pattern, which this repo has now hit three times: > We enumerate one channel and then conclude about all of them. `secrets.sh` was audited, and the environment was missed (`SSH_AUTH_SOCK`). The environment was audited, and the filesystem was missed (#184: the forge key is a readable passphrase-free file). Now the environment has been audited again, and **argv** was missed. Each time the conclusion was "members are contained", and each time it was true only of the one channel that was looked at. ## Actions 1. **Rotate all four.** `AI_GATEWAY_TOKEN` is already on #159. `AWS_ACCESS_KEY_ID` is the AdministratorAccess key. Add `CLAUDE_CODE_MESSAGING_TOKEN` and `N8N_ENCRYPTION_KEY`. 2. **Fix the launch config** so `chrome-devtools-mcp` receives its environment as an environment, not as argv. This is operator config, outside this repo. 3. **Add argv to the threat model** in whatever we write about member containment, and say plainly that argv beats every environment control we have. ## A rule for agents working in this repo `ps`, `pgrep -fl` and `ps -f` print argv, and argv can hold secrets. Add them to the list next to `git remote -v` and `${VAR:-x}`. To inspect processes, extract names and never values: ```bash pgrep -fl <pattern> | grep -oE '[A-Z][A-Z0-9_]{2,}=' | sed 's/=$//' | sort -u ``` `grep -o` on a pattern ending at `=` cannot emit the value, so this is safe by construction.
Author
Owner

Re-measured on the Mac, 2026-09-10. The exposure no longer reproduces, and I nearly reported that it had got worse.

The measurement

fleetd.jar          1 process,  0 env-names in argv
herdr               2 processes, 0 env-names in argv
chrome-devtools-mcp 4 processes, 0 env-names in argv   (pid 34827, 34894, 91257, 91287)

Across every process on the host, ps -axwwo pid=,args= finds 5 environment names in argv and 0 that are credential-shaped. The four names this ticket was filed for — AI_GATEWAY_TOKEN, AWS_ACCESS_KEY_ID, CLAUDE_CODE_MESSAGING_TOKEN, N8N_ENCRYPTION_KEY — are not in any process's argv now.

So action 2 is done: whoever owns the Claude Code MCP config fixed the chrome-devtools-mcp launch, and it now receives its environment as an environment. Actions 1 (rotation) belongs to #159 and #182 and is outside this repo. Nothing here is left to fix in code.

The part worth keeping: the check has a trap, and it fakes a positive

My first pass used this ticket's own suggested command and it told me the leak had spread to every claude process, including AWS_SECRET_ACCESS_KEY:

$ pgrep -fl claude | grep -oE '[A-Z][A-Z0-9_]{2,}=' | sed 's/=$//' | sort -u
AI_GATEWAY_TOKEN
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
BESZEL_ADMIN_PASSWORD
...

That is a false positive, and it is one this ticket set up. pgrep -fl prints the command line of the process running the search. My grep pattern contains the words TOKEN, KEY, SECRET and PASSWORD, and a shell command earlier in the session quoted this ticket's list of names. pgrep -fl matched my own argv and handed the names back to me.

Two counts, same host, same minute:

command env-names in argv credential-shaped
pgrep -fl claude 12 8
ps -axwwo args= (every process) 5 0

ps is right. I confirmed it by taking the single pid pgrep blamed and reading it with ps -wwo args= -p <pid>: 0 credential-shaped names.

A credential hunt that includes the hunter in its search space will find the credentials it is hunting for. Read against a ticket that lists the expected names, that output is indistinguishable from a real finding — and I was one sentence from reporting a live exposure of the operator's AWS secret key.

The safe form, names only:

ps -axwwo pid=,args= | grep -oE '[A-Z][A-Z0-9_]{2,}=' | sort -u | grep -E 'TOKEN|KEY|SECRET|PASSWORD'
ps -wwo args= -p <pid>  | grep -oE '[A-Z][A-Z0-9_]{2,}=' | sed 's/=$//' | sort -u

grep -o on a pattern ending at = cannot emit a value, so both are safe by construction. The rule in this ticket's last section should say ps, not pgrep -fl. I have not edited the body; this comment is the correction.

Action 3 was already done

The threat model does cover argv, in wiki chapter 16, "The one honest limitation: argv is world-readable". It names the exact seam (WorkspaceControl.createTab/splitPane pass env as a JSON params entry, never appended to argv) and is honest that the guarantee covers fleetd, herdr and claude and nothing else.

I added the detection trap above to that chapter, since it is an operational fact anyone re-running this audit will hit. Pushed as 967add3 on the wiki remote.

What this ticket taught, beyond argv

It was filed for the pattern "we enumerate one channel and conclude about all of them". The re-measurement adds a second: we enumerate with a tool that is inside the thing being enumerated. Same shape one level up — the observer was in the observed set. Closing.

**Re-measured on the Mac, 2026-09-10. The exposure no longer reproduces, and I nearly reported that it had got worse.** ## The measurement ``` fleetd.jar 1 process, 0 env-names in argv herdr 2 processes, 0 env-names in argv chrome-devtools-mcp 4 processes, 0 env-names in argv (pid 34827, 34894, 91257, 91287) ``` Across every process on the host, `ps -axwwo pid=,args=` finds **5** environment names in `argv` and **0** that are credential-shaped. The four names this ticket was filed for — `AI_GATEWAY_TOKEN`, `AWS_ACCESS_KEY_ID`, `CLAUDE_CODE_MESSAGING_TOKEN`, `N8N_ENCRYPTION_KEY` — are not in any process's `argv` now. So **action 2 is done**: whoever owns the Claude Code MCP config fixed the `chrome-devtools-mcp` launch, and it now receives its environment as an environment. Actions 1 (rotation) belongs to #159 and #182 and is outside this repo. Nothing here is left to fix in code. ## The part worth keeping: the check has a trap, and it fakes a positive My first pass used this ticket's own suggested command and it told me the leak had spread to every `claude` process, including `AWS_SECRET_ACCESS_KEY`: ``` $ pgrep -fl claude | grep -oE '[A-Z][A-Z0-9_]{2,}=' | sed 's/=$//' | sort -u AI_GATEWAY_TOKEN AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY BESZEL_ADMIN_PASSWORD ... ``` That is a false positive, and it is one this ticket set up. **`pgrep -fl` prints the command line of the process running the search.** My grep pattern contains the words `TOKEN`, `KEY`, `SECRET` and `PASSWORD`, and a shell command earlier in the session quoted this ticket's list of names. `pgrep -fl` matched my own `argv` and handed the names back to me. Two counts, same host, same minute: | command | env-names in argv | credential-shaped | |---|---|---| | `pgrep -fl claude` | 12 | 8 | | `ps -axwwo args=` (every process) | 5 | 0 | `ps` is right. I confirmed it by taking the single pid `pgrep` blamed and reading it with `ps -wwo args= -p <pid>`: 0 credential-shaped names. **A credential hunt that includes the hunter in its search space will find the credentials it is hunting for.** Read against a ticket that lists the expected names, that output is indistinguishable from a real finding — and I was one sentence from reporting a live exposure of the operator's AWS secret key. The safe form, names only: ```bash ps -axwwo pid=,args= | grep -oE '[A-Z][A-Z0-9_]{2,}=' | sort -u | grep -E 'TOKEN|KEY|SECRET|PASSWORD' ps -wwo args= -p <pid> | grep -oE '[A-Z][A-Z0-9_]{2,}=' | sed 's/=$//' | sort -u ``` `grep -o` on a pattern ending at `=` cannot emit a value, so both are safe by construction. **The rule in this ticket's last section should say `ps`, not `pgrep -fl`.** I have not edited the body; this comment is the correction. ## Action 3 was already done The threat model does cover argv, in wiki chapter 16, "The one honest limitation: argv is world-readable". It names the exact seam (`WorkspaceControl.createTab`/`splitPane` pass `env` as a JSON `params` entry, never appended to argv) and is honest that the guarantee covers `fleetd`, herdr and `claude` and nothing else. I added the detection trap above to that chapter, since it is an operational fact anyone re-running this audit will hit. Pushed as `967add3` on the wiki remote. ## What this ticket taught, beyond argv It was filed for the pattern "we enumerate one channel and conclude about all of them". The re-measurement adds a second: **we enumerate with a tool that is inside the thing being enumerated.** Same shape one level up — the observer was in the observed set. Closing.
ltms closed this issue 2026-09-10 02:11:28 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#190