diff --git a/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java b/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
index 652b28d..e1ff6e6 100644
--- a/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
+++ b/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
@@ -495,7 +495,7 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
*
Additive, not a rewrite. {@code .claude.json} is large (tens of KB, dozens of
* projects) and Claude Code itself rewrites it while running, so this reads the file as a JSON
* tree (missing or unreadable → treated as an empty object) and changes only
- * {@code projects..hasTrustDialogAccepted} / {@code .hasCompletedProjectOnboarding} —
+ * {@code projects..hasTrustDialogAccepted} (that key alone — see fleetd #247) —
* every other top-level key and every other project entry is written back untouched. Only the
* one project entry for {@code cwd} is replaced/created; an existing entry for a DIFFERENT cwd
* (or the operator's own project history) is never touched.
@@ -558,8 +558,18 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
if (!(projectNode instanceof ObjectNode)) {
projects.set(cwd, project);
}
+ // fleetd #247: ONLY hasTrustDialogAccepted. We used to write
+ // hasCompletedProjectOnboarding beside it; do not put it back. Measured on
+ // 2026-09-03, minutes after a live spawn seeded this file: 28 of 28 project
+ // entries carried hasTrustDialogAccepted and 0 of 28 carried the onboarding key
+ // — including the 27 entries Claude Code wrote for itself. Claude Code
+ // normalises the whole file when it saves and drops that key every time, so
+ // writing it achieved nothing except making the next reader think it mattered.
+ // The member reached idle with the trust flag alone, which is the only outcome
+ // this seed exists for. If a future Claude Code needs the second flag the
+ // symptom returns as the trust dialog fleetd #149 describes — re-measure then,
+ // do not restore it on a guess.
project.put("hasTrustDialogAccepted", true);
- project.put("hasCompletedProjectOnboarding", true);
writeAtomically(target, TRUST_JSON.writerWithDefaultPrettyPrinter().writeValueAsString(root));
} catch (Exception e) {
log.debug("cannot seed workspace-trust entry for cwd '{}' into '{}'", cwd, target, e);
diff --git a/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java b/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
index ea5f991..d995775 100644
--- a/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
+++ b/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
@@ -2159,8 +2159,7 @@ class ClaudeCodeLauncherTest {
}
JsonNode root = new ObjectMapper().readTree(claudeJson.toFile());
JsonNode project = root.path("projects").path(worktree.toString());
- seededBeforeStart.set(project.path("hasTrustDialogAccepted").asBoolean(false)
- && project.path("hasCompletedProjectOnboarding").asBoolean(false));
+ seededBeforeStart.set(project.path("hasTrustDialogAccepted").asBoolean(false));
} catch (IOException e) {
seededBeforeStart.set(false);
}
@@ -2178,7 +2177,7 @@ class ClaudeCodeLauncherTest {
}
@Test
- void seedTrustDialogWritesBothTrustFlagsForTheResolvedCwd(
+ void seedTrustDialogWritesOnlyTheTrustFlagAndNotTheOnboardingKey(
@TempDir Path configDir, @TempDir Path worktree) throws Exception {
markAsProvisionedWorktree(worktree);
FakeHerdr herdr = new FakeHerdr();
@@ -2192,7 +2191,13 @@ class ClaudeCodeLauncherTest {
JsonNode project = new ObjectMapper().readTree(claudeJson.toFile())
.path("projects").path(worktree.toString());
assertTrue(project.path("hasTrustDialogAccepted").asBoolean(false));
- assertTrue(project.path("hasCompletedProjectOnboarding").asBoolean(false));
+ // fleetd #247: the onboarding key must NOT be written. Claude Code strips it on every
+ // save (measured: 0 of 28 live entries had it, including its own), so writing it only
+ // adds a contested key to a file two processes share. This assertion is the guard that
+ // stops it coming back as a plausible-looking "completeness" fix.
+ assertFalse(project.has("hasCompletedProjectOnboarding"),
+ "hasCompletedProjectOnboarding must not be written — Claude Code drops it on "
+ + "every save, and the member reaches idle on hasTrustDialogAccepted alone");
}
/**
@@ -2238,7 +2243,6 @@ class ClaudeCodeLauncherTest {
JsonNode mine = root.path("projects").path(worktree.toString());
assertTrue(mine.path("hasTrustDialogAccepted").asBoolean(false));
- assertTrue(mine.path("hasCompletedProjectOnboarding").asBoolean(false));
}
/**