ClaudeCodeLauncher writes the member's charter file into fleetd's own java.io.tmpdir — the #219 site-1 shape, third instance #222

Closed
opened 2026-09-01 10:25:49 +02:00 by ltms · 1 comment
Owner

Found by the #219 worker while fixing OpenCodeLauncher, reported as out of scope. I checked it in the code on main.

Third instance of one shape: fleetd reads or writes its own process's filesystem to decide something about a member. #213 was the ZDOTDIR scrub, #219 was OpenCodeLauncher's config and discovery roots, this is the claude-code adapter.

The site

ClaudeCodeLauncher#writeCharterFile (around line 465):

Files.createTempFile(...)   // no directory argument -> java.io.tmpdir

On macOS java.io.tmpdir is the per-user $TMPDIR under /var/folders/..., mode drwx------ (0700), and the temp file itself is 0600. Both are resolved against fleetd's process, not the member's.

Why this one is sharper than #219 site 1

This path is newer and more load-bearing than it looks. Before #220 (c3fa113) the charter usually travelled inline on --append-system-prompt, and the file was the exception. #220 made the file the only delivery path, always, to keep the launch command inside the pane's 1024-byte line. So under memberHerdrSocket: every claude-code member would be handed --append-system-prompt-file <a path it cannot read>.

I have not measured what claude-code does with an unreadable --append-system-prompt-file. Two outcomes, and neither is good:

  • it exits on the bad argument — the pane closes and the spawn dies at the readiness gate, the exact silent failure #220 spent a session diagnosing; or
  • it starts without the charter — a member with no reply charter, which per the layering table is the one rule that must survive with no repo. That member ends its turn with no fleet_reply and the sender silently receives nothing.

Whoever takes this should measure which, and say so in the ticket. The fix does not depend on the answer, but the severity does.

The fix already exists — reuse it, do not write a third copy

#219 landed EnvAllowListScrub.shareWithGroup(dir, group) (package-private, deliberately named generically) and HerdrPeerLauncher#memberHerdrSocketConfigured() / memberScrubParentDir() / memberGroup(). OpenCodeLauncher#configParentDir() is the worked example: when memberHerdrSocket: is set, generate under worktreeRoot and share with worktreeGroup:; when it is absent, keep today's path byte-identical.

Follow #219's refusal decision, not #213's degrade decision. The ZDOTDIR scrub is a credential control, and a degraded control still has value. A charter is not a control — it is the member's turn contract. A member that cannot read it is not a degraded member, it is a broken one. Refuse the spawn, naming the missing config key.

Not live today

memberHerdrSocket: is absent on this host, so this is not currently wrong. It is a blocker for that rollout, alongside #213 (done) and #219.

Acceptance

  • With memberHerdrSocket: set, the charter file is not under java.io.tmpdir, and is created with the same group sharing #213/#219 established.
  • With memberHerdrSocket: set and worktreeRoot/worktreeGroup missing, the spawn is refused with a message naming the missing key — not a member launched without its charter.
  • With memberHerdrSocket: absent, the path is byte-identical to today. Say in the PR how you convinced yourself of this; it is the criterion that protects the live fleet.
  • The measured answer to "what does claude-code do with an unreadable --append-system-prompt-file" is recorded, in the ticket or the javadoc.
  • Watch each test fail before keeping it.

Related

#219 (the same shape, the mechanism to reuse) · #213 (the first instance) · #220 (why this path is now unconditional) · #185 (parent).

Found by the #219 worker while fixing `OpenCodeLauncher`, reported as out of scope. I checked it in the code on `main`. Third instance of one shape: **fleetd reads or writes its own process's filesystem to decide something about a member.** #213 was the ZDOTDIR scrub, #219 was `OpenCodeLauncher`'s config and discovery roots, this is the claude-code adapter. ## The site `ClaudeCodeLauncher#writeCharterFile` (around line 465): ```java Files.createTempFile(...) // no directory argument -> java.io.tmpdir ``` On macOS `java.io.tmpdir` is the per-user `$TMPDIR` under `/var/folders/...`, mode `drwx------` (0700), and the temp file itself is `0600`. Both are resolved against **fleetd's** process, not the member's. ## Why this one is sharper than #219 site 1 This path is **newer and more load-bearing than it looks**. Before #220 (`c3fa113`) the charter usually travelled inline on `--append-system-prompt`, and the file was the exception. #220 made the file the **only** delivery path, always, to keep the launch command inside the pane's 1024-byte line. So under `memberHerdrSocket:` every claude-code member would be handed `--append-system-prompt-file <a path it cannot read>`. I have not measured what claude-code does with an unreadable `--append-system-prompt-file`. Two outcomes, and neither is good: - it exits on the bad argument — the pane closes and the spawn dies at the readiness gate, the exact silent failure #220 spent a session diagnosing; or - it starts without the charter — a member with no reply charter, which per the layering table is the one rule that must survive with no repo. That member ends its turn with no `fleet_reply` and the sender silently receives nothing. **Whoever takes this should measure which, and say so in the ticket.** The fix does not depend on the answer, but the severity does. ## The fix already exists — reuse it, do not write a third copy #219 landed `EnvAllowListScrub.shareWithGroup(dir, group)` (package-private, deliberately named generically) and `HerdrPeerLauncher#memberHerdrSocketConfigured() / memberScrubParentDir() / memberGroup()`. `OpenCodeLauncher#configParentDir()` is the worked example: when `memberHerdrSocket:` is set, generate under `worktreeRoot` and share with `worktreeGroup:`; when it is absent, keep today's path byte-identical. **Follow #219's refusal decision, not #213's degrade decision.** The ZDOTDIR scrub is a credential *control*, and a degraded control still has value. A charter is not a control — it is the member's turn contract. A member that cannot read it is not a degraded member, it is a broken one. Refuse the spawn, naming the missing config key. ## Not live today `memberHerdrSocket:` is absent on this host, so this is not currently wrong. It is a blocker for that rollout, alongside #213 (done) and #219. ## Acceptance - With `memberHerdrSocket:` set, the charter file is **not** under `java.io.tmpdir`, and is created with the same group sharing #213/#219 established. - With `memberHerdrSocket:` set and `worktreeRoot`/`worktreeGroup` missing, the spawn is refused with a message naming the missing key — not a member launched without its charter. - With `memberHerdrSocket:` absent, the path is byte-identical to today. Say in the PR how you convinced yourself of this; it is the criterion that protects the live fleet. - The measured answer to "what does claude-code do with an unreadable `--append-system-prompt-file`" is recorded, in the ticket or the javadoc. - Watch each test fail before keeping it. ## Related #219 (the same shape, the mechanism to reuse) · #213 (the first instance) · #220 (why this path is now unconditional) · #185 (parent).
Author
Owner

Fixed in PR #229, merged to main (2a434ce).

writeCharterFile now branches on memberHerdrSocketConfigured(). Absent — today's only live mode — is the literal unmodified Files.createTempFile("fleetd-role-charter-", ".md") call kept in its own branch, deliberately not routed through a shared "default parent dir" helper, so there is no new path that could drift. Present: a per-spawn directory under worktreeRoot, shared with worktreeGroup through the existing EnvAllowListScrub.shareWithGroup, and a refusal naming the missing key when either is unconfigured. No third copy of the policy.

The measured answer the ticket asked for

Against the real binary (claude 2.1.258), invoked with a bogus API key so anything else would need this check to pass first:

chmod 000 file : Error reading append system prompt file: EACCES: permission denied, open '<path>'
                 exit 1, immediately
nonexistent    : Error: Append system prompt file not found: <path>
                 exit 1, immediately

So it is the loud failure of the two I listed. claude checks the file before it touches auth or the network. Pre-fix, under memberHerdrSocket: the pane would have exited at once and surfaced as CB-306's "did not reach injectable state" — the same unexplained-timeout shape #220 spent a session diagnosing, with nothing in the message pointing at a temp directory. It would not have produced a silently charter-less member that never calls fleet_reply. Still a real defect, but the recoverable direction. Recorded in the writeCharterFile javadoc.

One thing this ticket caused

The worker copied currentUserGroup() from OpenCodeLauncherTest into ClaudeCodeLauncherTest, which made a third copy of the #225 bug while #225 was being fixed in a worktree it could not see. Caught at review and fixed on the same branch. Three test classes now carry this helper; collapsing it into one shared test utility is worth doing next time someone is in these files.

Verified on main after merge: BUILD SUCCESS, 1100 tests, 0 failures, 0 skipped, from both a home checkout and a wheel-grouped copy under /private/tmp. A live claude-code spawn on the redeployed daemon (pid 29196, jar 4a0e2c8f0eef) reached idle — since #220 the charter travels only as a file, so that proves the new path end to end.

Fixed in PR #229, merged to `main` (2a434ce). `writeCharterFile` now branches on `memberHerdrSocketConfigured()`. Absent — today's only live mode — is the literal unmodified `Files.createTempFile("fleetd-role-charter-", ".md")` call kept in its own branch, deliberately not routed through a shared "default parent dir" helper, so there is no new path that could drift. Present: a per-spawn directory under `worktreeRoot`, shared with `worktreeGroup` through the existing `EnvAllowListScrub.shareWithGroup`, and a refusal naming the missing key when either is unconfigured. No third copy of the policy. ## The measured answer the ticket asked for Against the real binary (claude 2.1.258), invoked with a bogus API key so anything else would need this check to pass first: ``` chmod 000 file : Error reading append system prompt file: EACCES: permission denied, open '<path>' exit 1, immediately nonexistent : Error: Append system prompt file not found: <path> exit 1, immediately ``` So it is the **loud** failure of the two I listed. claude checks the file before it touches auth or the network. Pre-fix, under `memberHerdrSocket:` the pane would have exited at once and surfaced as CB-306's "did not reach injectable state" — the same unexplained-timeout shape #220 spent a session diagnosing, with nothing in the message pointing at a temp directory. It would **not** have produced a silently charter-less member that never calls `fleet_reply`. Still a real defect, but the recoverable direction. Recorded in the `writeCharterFile` javadoc. ## One thing this ticket caused The worker copied `currentUserGroup()` from `OpenCodeLauncherTest` into `ClaudeCodeLauncherTest`, which made a **third** copy of the #225 bug while #225 was being fixed in a worktree it could not see. Caught at review and fixed on the same branch. Three test classes now carry this helper; collapsing it into one shared test utility is worth doing next time someone is in these files. Verified on `main` after merge: BUILD SUCCESS, 1100 tests, 0 failures, 0 skipped, from both a home checkout and a `wheel`-grouped copy under `/private/tmp`. A live claude-code spawn on the redeployed daemon (pid 29196, jar `4a0e2c8f0eef`) reached `idle` — since #220 the charter travels only as a file, so that proves the new path end to end.
ltms closed this issue 2026-09-02 03:04:46 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#222