fix(#2665): watch the fallback root a scrubbing child actually resolves to

resolveLiveConfigRoots resolves what THIS process sees, and getGlobalConfigDir
is env-first -- so with an ambient CLAUDE_CONFIG_DIR the guard watched that
path. A spawned child does not see it: TEST_ENV_BASE blanks the config-location
vars precisely so the child cannot follow them, and a blanked var is falsy, so
the child resolves its HOME-derived root instead.

A child that blanks the var and does NOT also sandbox HOME therefore writes into
the developer's real ~/.claude, which the guard was not watching. That is this
PR's own escape route, taken one process deeper -- and the guard is the artifact
that is supposed to make it loud.

Both resolutions are now unioned: the ambient one, and the fallback one obtained
by handing the REAL descriptor resolver an EMPTY env. Deriving it that way is
deliberate -- a hand-listed copy of the scrub set inside the guard is a second
list to drift, which is the defect this PR spent three rounds closing one layer
up. grok resolves through a hardcoded branch rather than a descriptor, so its
fallback is stated explicitly for the same reason it is named in the ambient
loop.

Addresses review finding: Blocker 3.
This commit is contained in:
0xdhx
2026-08-06 16:31:40 -05:00
parent 6a1fbf96fd
commit e82a15a852
2 changed files with 67 additions and 1 deletions

View File

@@ -43,6 +43,34 @@ describe('#2665: live-config hermeticity guard', () => {
}
});
test('watches the HOME-derived fallback root, not only the ambient one', () => {
// #2665 round 5: the guard resolves env-first, so it sees the AMBIENT root.
// A child that blanks CLAUDE_CONFIG_DIR (which is exactly what TEST_ENV_BASE
// does) falls back to <HOME>/.claude instead. Watching only the ambient path
// leaves that fallback unwatched -- the escape route this PR closes, one
// process deeper.
const ambient = tmpRoot();
const fakeHome = tmpRoot();
const saved = process.env.CLAUDE_CONFIG_DIR;
try {
process.env.CLAUDE_CONFIG_DIR = ambient;
const roots = resolveLiveConfigRoots({ os: { homedir: () => fakeHome } });
assert.ok(
roots.includes(path.resolve(ambient)),
`ambient root missing from ${JSON.stringify(roots)}`,
);
assert.ok(
roots.includes(path.resolve(path.join(fakeHome, '.claude'))),
`HOME-derived fallback root missing from ${JSON.stringify(roots)}`,
);
} finally {
if (saved === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = saved;
cleanup(ambient);
cleanup(fakeHome);
}
});
test('a clean run produces no violations', () => {
const root = tmpRoot();
try {