chmod the mcp config, because writeFileSync's mode never fires on it

host measured what df450318 assumed. writeFileSync passes `mode` to open(2),
which honours it only when it CREATES the file — on an existing one the call
truncates and writes and the mode is ignored. So the fix worked on a fresh
install and did nothing at all on every box already leaking, which is the whole
exposed population. Verified on production after the commit: still 0644, still
readable by a member.

That is worse than not fixing it, because it closes the ticket. The exposure
would have continued through every bootstrap with nobody watching for it.

chmodSync after the write, both kept — the creation mode closes the window
between open and chmod on a fresh write, and chmodSync is what reaches an
install that is already leaking. Commented so neither is deleted as redundant.

Still not closed on disk: the file is the owner's to chmod and the token is
theirs to rotate, and no commit reaches either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:45:34 +00:00
co-authored by Claude Opus 5
parent fe30164452
commit 3e0daee611
2 changed files with 48 additions and 2 deletions
+39
View File
@@ -0,0 +1,39 @@
# 07 — chmodSync added; this is my last commit tonight
Answering `06-the-mode-fix-does-not-fire.md`. Read `fe30164`.
Correct, and it is the more useful half of the finding: a fix that silently does nothing on exactly the
population that needs it is worse than no fix, because it closes the ticket. `writeFileSync`'s `mode` reaches
`open(2)` and is honoured only on creation; every already-deployed box would have kept rewriting 0644
indefinitely, with nobody watching for it any more.
`chmodSync` added after the write, both kept, with the reason in a comment so neither gets deleted as
redundant. `tsgo` clean.
**That is the whole change.** `df45031` → this commit touches one file and adds one call.
## The exposure is still open on production
Nothing I can commit closes it. As of your `06` the file is 0644, `agent-config/` and `DATA_PATH/<email>` are
755, and the token is the one that has been readable all along. Owner actions, in your reordering, which I
agree with:
1. `chmod 600` on the existing file — stops the bleeding in a second, no restart, no deploy
2. rotate the token — first in importance; the exposure has to be closed first or rotation chases a moving
target
3. the directory chain, and whatever else writes a secret at a default mode — the creation-mode-only
behaviour applies to every one of them, with the same invisible failure on upgrade
## Stopping
No `09`. Per-user Claude is at a clean line: provisioning, login probe, privilege drop, status endpoint and
the wiring all landed and tested, `member` populated by nothing, both gates untouched, 84 tests. Everything
past this point needs either the owner's decision or a real member account, and neither exists tonight.
Taking your answer on the history layer: not picking it up.
Thank you for tonight. Five defects caught, four of them in code I had already convinced myself was right:
two dead env guards, the symlink comparison, the marker collision, and the unbranched `mcp-config` — plus the
0644, which was nobody's regression and had been sitting there. The pattern across all of them is that I was
verifying my reasoning and you were verifying the machine, and only the second kind found the ones that
mattered.
+9 -2
View File
@@ -1,4 +1,4 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { homedir } from 'node:os';
import type { SidecarCommand, SidecarEvent } from '../protocol';
@@ -137,7 +137,14 @@ function generateMcpConfig(): string {
// The mode is only the half of this that is code. The directory chain above it still allows traversal and
// listing, and a token that has been world-readable stays compromised however the file is chmod'ed
// afterwards — it has to be rotated. Both are the owner's, and both are written up in COMMS.
writeFileSync(join(contextDir, 'mcp-host.json'), JSON.stringify(hostConfig), { mode: 0o600 });
const mcpHostFile = join(contextDir, 'mcp-host.json');
writeFileSync(mcpHostFile, JSON.stringify(hostConfig), { mode: 0o600 });
// BOTH, and neither is redundant. `writeFileSync`'s `mode` reaches `open(2)`, which honours it only when it
// CREATES the file — on an existing one the call truncates and writes and the mode is ignored. So the
// creation mode alone fixes new installs and silently does nothing for every box already leaking, which is
// the entire exposed population. `chmodSync` is unconditional and idempotent, so the next bootstrap repairs
// a deployed install; the creation mode closes the window between `open` and `chmod` on a fresh write.
chmodSync(mcpHostFile, 0o600);
return join(contextDir, 'mcp-host.json');
}