Two test fixtures write to the operator's real ~/.claude.json on every run (116 dead entries accumulated) #258

Closed
opened 2026-09-03 09:01:28 +02:00 by ltms · 0 comments
Owner

What happens

ClaudeCodeLauncher.seedTrustDialog writes <configDir>/.claude.json, or ~/.claude.json when the profile sets no configDir.

Two fixtures in ClaudeCodeLauncherTest build a worktree-shaped @TempDir (a real .git FILE) and leave configDir null. So on every test run, two project entries land in the operator's real ~/.claude.json:

  • writeIdeOverlayWritesClaudeLocalAndAddsItToCommonInfoExclude
  • writeIdeOverlayPinsModuleDirWhenIdeProjectDirSet

The third overlay test is safe only by accident — its .git is a directory, so the gate blocks it.

Why the #149 gate did not stop this

The isProvisionedWorktree(cwd) gate added after #149 closes the case where a fixture leaves cwd unset and it falls back to user.dir. It does not close the opposite case: a fixture that builds a worktree-shaped dir on purpose. There the gate opens, and a null configDir still points the write at the real home.

Measured

On this host, 2026-09-03:

total projects: 134
junit-temp projects: 116
of those, still existing on disk: 0
fields on junit entries: {'hasTrustDialogAccepted': 116, 'hasCompletedProjectOnboarding': 84}

84 carry hasCompletedProjectOnboarding, which #247 removed today — so 32 were written by the current code. This is ongoing, not history.

Blast radius measured by running the whole suite with -Duser.home=<tempdir>: exactly 2 entries per full run, both from the two fixtures above. No other test class writes a home.

Fix

  1. ideProfile and ideProfileModule now take configDir as a first, mandatory parameter, so every fixture states where the trust seed goes. Its javadoc says: pass a @TempDir whenever cwd has a .git FILE.
  2. New differential guard noFixtureSeededTheDefaultClaudeJson — @BeforeAll snapshots the temp-dir project keys in the default .claude.json, @AfterAll fails on any key the class added. Differential rather than absolute, because an absolute check would fail on every host still carrying the historical entries, and a check that fails for a reason nobody can fix gets deleted rather than fixed.

Rejected: a blanket -Duser.home redirect in surefire

It looks like the obvious structural fix and it is a trap. EnvAllowListScrubTest deliberately tests the credential scrub against the operator's real login chain, guarded by assumeTrue($HOME/.zshrc exists). Redirecting user.home for the whole suite would turn those two security tests into silent skips — trading a hygiene bug for lost coverage, with nothing reporting the loss.

Proof the guard works

Put the bug back on one fixture, keep the other fixed:

[ERROR] ClaudeCodeLauncherTest.noFixtureSeededTheDefaultClaudeJson:264
  a fixture in this class seeded the DEFAULT .claude.json ... with 1 temp-dir project
  entry/entries: [/var/folders/.../T/junit-13505215142980669683/worktree].
  Give that fixture's profile a @TempDir configDir — see ideProfile's javadoc.

0 compile errors, mutation reverted and confirmed byte-identical with diff -q.

After the fix, a full run under a fake home creates no .claude.json at all. mvn clean install: 0 compile errors, 1255 tests, BUILD SUCCESS.

Still owed (operator's file, not touched here)

The 116 dead entries are still in ~/.claude.json. They are harmless but they bloat a file Claude Code rewrites on every save. Removing them is a separate, operator-approved cleanup — this change only stops new ones being added.

## What happens `ClaudeCodeLauncher.seedTrustDialog` writes `<configDir>/.claude.json`, or `~/.claude.json` when the profile sets no `configDir`. Two fixtures in `ClaudeCodeLauncherTest` build a worktree-shaped `@TempDir` (a real `.git` FILE) and leave `configDir` null. So on every test run, two project entries land in the **operator's real `~/.claude.json`**: - `writeIdeOverlayWritesClaudeLocalAndAddsItToCommonInfoExclude` - `writeIdeOverlayPinsModuleDirWhenIdeProjectDirSet` The third overlay test is safe only by accident — its `.git` is a directory, so the gate blocks it. ## Why the #149 gate did not stop this The `isProvisionedWorktree(cwd)` gate added after #149 closes the case where a fixture leaves `cwd` unset and it falls back to `user.dir`. It does not close the opposite case: a fixture that builds a worktree-shaped dir **on purpose**. There the gate opens, and a null `configDir` still points the write at the real home. ## Measured On this host, 2026-09-03: ``` total projects: 134 junit-temp projects: 116 of those, still existing on disk: 0 fields on junit entries: {'hasTrustDialogAccepted': 116, 'hasCompletedProjectOnboarding': 84} ``` 84 carry `hasCompletedProjectOnboarding`, which #247 removed today — so 32 were written by the **current** code. This is ongoing, not history. Blast radius measured by running the whole suite with `-Duser.home=<tempdir>`: exactly **2** entries per full run, both from the two fixtures above. No other test class writes a home. ## Fix 1. `ideProfile` and `ideProfileModule` now take `configDir` as a first, mandatory parameter, so every fixture states where the trust seed goes. Its javadoc says: pass a `@TempDir` whenever `cwd` has a `.git` FILE. 2. New differential guard `noFixtureSeededTheDefaultClaudeJson` — `@BeforeAll` snapshots the temp-dir project keys in the default `.claude.json`, `@AfterAll` fails on any key the class **added**. Differential rather than absolute, because an absolute check would fail on every host still carrying the historical entries, and a check that fails for a reason nobody can fix gets deleted rather than fixed. ## Rejected: a blanket `-Duser.home` redirect in surefire It looks like the obvious structural fix and it is a trap. `EnvAllowListScrubTest` deliberately tests the credential scrub against the operator's **real** login chain, guarded by `assumeTrue($HOME/.zshrc exists)`. Redirecting `user.home` for the whole suite would turn those two security tests into silent skips — trading a hygiene bug for lost coverage, with nothing reporting the loss. ## Proof the guard works Put the bug back on one fixture, keep the other fixed: ``` [ERROR] ClaudeCodeLauncherTest.noFixtureSeededTheDefaultClaudeJson:264 a fixture in this class seeded the DEFAULT .claude.json ... with 1 temp-dir project entry/entries: [/var/folders/.../T/junit-13505215142980669683/worktree]. Give that fixture's profile a @TempDir configDir — see ideProfile's javadoc. ``` 0 compile errors, mutation reverted and confirmed byte-identical with `diff -q`. After the fix, a full run under a fake home creates **no `.claude.json` at all**. `mvn clean install`: 0 compile errors, 1255 tests, BUILD SUCCESS. ## Still owed (operator's file, not touched here) The 116 dead entries are still in `~/.claude.json`. They are harmless but they bloat a file Claude Code rewrites on every save. Removing them is a separate, operator-approved cleanup — this change only stops new ones being added.
ltms closed this issue 2026-09-03 09:01:54 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#258