"left no scrub report" has a third, benign cause it does not name — a group-shared ZDOTDIR cannot receive the receipt #384

Closed
opened 2026-09-09 20:26:40 +02:00 by ltms · 1 comment
Owner

Found by reading the code while helping the fleet01 lead diagnose #383's neighbour. Not reachable on either host today — see Reachability. Filing it low so it is not lost, per the "a defect on paper is not a reachable defect" rule: the path exists in shipped code and config, but nobody runs that config yet.

The warning

HerdrPeerLauncher.releaseZdotdir reads the scrub receipt and, when it is missing, logs:

memberCredentials allow-list: pane {} left no scrub report in {} — the environment scrub cannot be confirmed to have run. Either the pane ended before its shell finished starting, or its shell never read our generated startup files, in which case that member saw the full host environment.

That text names exactly two readings, and the second is a credential exposure. Its javadoc is explicit that this is WARN and not debug on purpose, because "logging this at debug is how a control that silently stopped working stays unnoticed". That reasoning is right.

The third reading

There is a third cause, and the warning does not mention it: the scrub ran perfectly and could not write its receipt.

EnvAllowListScrub.shareWithGroup (fleetd #213, for members running as a different OS user) sets:

setGroupAndPermissions(dir, principal, "rwxr-x---");   // group: r-x — no WRITE
    ...
    setGroupAndPermissions(file, principal, "rw-r-----");

The generated scrub.zsh blanks the environment first, and only then writes its receipt:

{ for _cb633_n in "${_cb633_blank[@]}"; do export "$_cb633_n="; done; } 2>/dev/null

integer _cb633_kept=$(( _cb633_total - ${#_cb633_blank} ))
{
  print -r -- "allowed $_cb633_kept of $_cb633_total"
  for _cb633_n in "${_cb633_blank[@]}"; do print -r -- "$_cb633_n"; done
} > "$ZDOTDIR/scrub-report.txt" 2>/dev/null

Creating scrub-report.txt needs write permission on the directory. A member in the shared group has r-x. So the write fails, 2>/dev/null swallows it, and readReport returns null — while the protection itself worked.

This is already documented as deliberate, in shareWithGroup's javadoc:

the scrub script's own report write inside the pane fails closed rather than open — see scrib.zsh's trailing 2>/dev/null — which HerdrPeerLauncher#releaseZdotdir already treats as "cannot be confirmed to have run" rather than success.

So the behaviour is intended. The message is what is wrong.

Why it still matters

On a host configured with memberHerdrSocket + worktreeGroup, this WARN fires on every pane release, forever, saying the member may have seen the full host environment — when nothing is wrong. An alarm that always fires is not an alarm. That defeats the exact purpose the javadoc states: keeping a silently-broken control noticeable. The one line that would report a real scrub failure becomes the line operators learn to skip.

It is also the harder direction to debug. The two hosts here disagree on this warning right now, and working out which reading applied took reading three files.

Reachability — measured 2026-09-09

Neither host can hit it today, because neither configures group sharing:

Mac      grep -nE '^\s*(memberHerdrSocket|worktreeGroup):' fleetd/fleetd.yaml   -> no matches
fleet01  same grep on its fleetd.yaml                                          -> no matches

Both therefore take the else branch in applyEnvironmentAllowListPolicy (parentDir = java.io.tmpdir, group = null), so shareWithGroup never runs. Evidence that the receipt path is healthy without group sharing, from the Mac's live log:

successful "allowed N of M" reports:  426
"left no scrub report":                 0

fleet01 does log left no scrub report, but it has no worktreeGroup, so that host's warnings are a different problem and are not explained by this ticket. Do not close its case with this one.

Suggested fix

releaseZdotdir already knows whether group sharing was applied for the pane (the launcher chose it at spawn). Carry that flag and branch the message:

  • group-shared ⇒ the receipt is expected to be unwritable. Say that, and say plainly that the scrub itself cannot be confirmed either way from here — do not imply exposure.
  • not shared ⇒ keep today's text unchanged; both of its readings still apply.

A better fix, if the receipt is worth keeping under sharing: write it somewhere the member can create a file, so the evidence actually survives. Excusing a missing receipt is weaker than getting one.

Either way a test should pin the two messages apart. Today EnvAllowListScrubTest covers readReport returning null for a directory without one, but nothing pins what the operator is then told.

Found by reading the code while helping the fleet01 lead diagnose #383's neighbour. **Not reachable on either host today** — see Reachability. Filing it low so it is not lost, per the "a defect on paper is not a reachable defect" rule: the path exists in shipped code and config, but nobody runs that config yet. ## The warning `HerdrPeerLauncher.releaseZdotdir` reads the scrub receipt and, when it is missing, logs: > memberCredentials allow-list: pane {} left no scrub report in {} — the environment scrub cannot be confirmed to have run. Either the pane ended before its shell finished starting, or **its shell never read our generated startup files, in which case that member saw the full host environment.** That text names exactly two readings, and the second is a credential exposure. Its javadoc is explicit that this is WARN and not debug on purpose, because "logging this at debug is how a control that silently stopped working stays unnoticed". That reasoning is right. ## The third reading There is a third cause, and the warning does not mention it: **the scrub ran perfectly and could not write its receipt.** `EnvAllowListScrub.shareWithGroup` (fleetd #213, for members running as a different OS user) sets: ```java setGroupAndPermissions(dir, principal, "rwxr-x---"); // group: r-x — no WRITE ... setGroupAndPermissions(file, principal, "rw-r-----"); ``` The generated `scrub.zsh` blanks the environment **first**, and only then writes its receipt: ```zsh { for _cb633_n in "${_cb633_blank[@]}"; do export "$_cb633_n="; done; } 2>/dev/null integer _cb633_kept=$(( _cb633_total - ${#_cb633_blank} )) { print -r -- "allowed $_cb633_kept of $_cb633_total" for _cb633_n in "${_cb633_blank[@]}"; do print -r -- "$_cb633_n"; done } > "$ZDOTDIR/scrub-report.txt" 2>/dev/null ``` Creating `scrub-report.txt` needs write permission on the directory. A member in the shared group has `r-x`. So the write fails, `2>/dev/null` swallows it, and `readReport` returns null — while the protection itself worked. This is **already documented as deliberate**, in `shareWithGroup`'s javadoc: > the scrub script's own report write inside the pane fails closed rather than open — see `scrib.zsh`'s trailing `2>/dev/null` — which `HerdrPeerLauncher#releaseZdotdir` already treats as "cannot be confirmed to have run" rather than success. So the behaviour is intended. The **message** is what is wrong. ## Why it still matters On a host configured with `memberHerdrSocket` + `worktreeGroup`, this WARN fires on **every** pane release, forever, saying the member may have seen the full host environment — when nothing is wrong. An alarm that always fires is not an alarm. That defeats the exact purpose the javadoc states: keeping a silently-broken control noticeable. The one line that would report a real scrub failure becomes the line operators learn to skip. It is also the harder direction to debug. The two hosts here disagree on this warning right now, and working out which reading applied took reading three files. ## Reachability — measured 2026-09-09 Neither host can hit it today, because neither configures group sharing: ``` Mac grep -nE '^\s*(memberHerdrSocket|worktreeGroup):' fleetd/fleetd.yaml -> no matches fleet01 same grep on its fleetd.yaml -> no matches ``` Both therefore take the `else` branch in `applyEnvironmentAllowListPolicy` (`parentDir = java.io.tmpdir`, `group = null`), so `shareWithGroup` never runs. Evidence that the receipt path is healthy without group sharing, from the Mac's live log: ``` successful "allowed N of M" reports: 426 "left no scrub report": 0 ``` fleet01 does log `left no scrub report`, but it has no `worktreeGroup`, so **that host's warnings are a different problem** and are not explained by this ticket. Do not close its case with this one. ## Suggested fix `releaseZdotdir` already knows whether group sharing was applied for the pane (the launcher chose it at spawn). Carry that flag and branch the message: - **group-shared** ⇒ the receipt is expected to be unwritable. Say that, and say plainly that the scrub itself cannot be confirmed either way from here — do not imply exposure. - **not shared** ⇒ keep today's text unchanged; both of its readings still apply. A better fix, if the receipt is worth keeping under sharing: write it somewhere the member can create a file, so the evidence actually survives. Excusing a missing receipt is weaker than getting one. Either way a test should pin the two messages apart. Today `EnvAllowListScrubTest` covers `readReport` returning null for a directory without one, but nothing pins what the operator is then told.
Author
Owner

Merged. I verified it myself rather than taking the worker's report.

What landed. generate() now pre-creates an empty scrub-report.txt, and shareWithGroup gives that one file rw-rw----. Every other file stays rw-r----- and the directory stays rwxr-x---, so no group write is granted on the directory. A failure to set the receipt's mode is swallowed and the spawn continues.

Build at the merged HEAD: Tests run: 1464, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS, exit 0. Run unpiped to a file.

The thing I checked before merging, because a pre-created file is a good way to silence the alarm it exists to raise. The receipt now always exists, so "the file is there" can no longer mean "the scrub ran". I read readReport rather than assuming: it returns null when the file is empty and when the first line does not start with allowed . So an untouched pre-created receipt still produces "no measurement available", and releaseZdotdir still warns. No regression. Anyone reading a receipt by hand needs to know this — an empty file means the scrub did not run.

Mutation, on the half the worker did not pin. The worker proved its own line by deleting the pre-created receipt. So I mutated the other branch instead: the else arm that keeps every non-receipt file at rw-r-----, changed to rw-rw----. That is the security half — the reason the directory does not get group write in the first place.

It was caught, by three tests that were already there:

EnvAllowListScrubTest.shareWithGroupCoversEveryFlatFileIncludingTheOptionalOnes:171
  opencode.json must be group-readable, never group-writable ==> expected: <rw-r-----> but was: <rw-rw---->
ClaudeCodeLauncherTest.memberHerdrSocketWithWorktreeRootAndGroupPutsCharterUnderWorktreeRootAndSharesIt:2185
  the charter file must be group-readable, never group-writable ==> expected: <rw-r-----> but was: <rw-rw---->
OpenCodeLauncherTest.memberHerdrSocketWithWorktreeRootAndGroupPutsConfigDirUnderWorktreeRootAndSharesIt:865

That is the outcome I want and do not often get: the invariant was pinned by a test named for its own denominator, covering every flat file including the optional ones. No gap this time.

One small fix on merge: five new javadoc lines came in with six leading spaces instead of five. Whitespace only, inside a comment.

One thing the change removed that is worth saying out loud. The old javadoc claimed the scrub's report write "fails closed rather than open". That is no longer true, and the worker correctly deleted the sentence. The receipt is now writable by the member, so it is evidence produced by the thing being checked. That is the deliberate trade this ticket asked for — without it the write always failed and the WARN fired forever — but it means a present, well-formed receipt proves the scrub ran only if you trust the member. It is a receipt, not an attestation.

And the reason this ticket is not the whole story: #388. #384 fixes the message and the receipt. It does not make the scrub run. On a host whose panes are neither login nor interactive shells, the scrub never executes at all, because it is sourced from .zshrc and .zlogin and never from .zshenv — the one file zsh always reads. Measured on fleet01 over 8 spawns by that host's lead. #388 has the evidence and the proposed fix.

So the receipt work here is still worth having: once #388 makes the scrub run everywhere, a present receipt becomes the normal case and a missing one becomes a real signal instead of the constant it is on that host today.

Merged. I verified it myself rather than taking the worker's report. **What landed.** `generate()` now pre-creates an empty `scrub-report.txt`, and `shareWithGroup` gives that one file `rw-rw----`. Every other file stays `rw-r-----` and the directory stays `rwxr-x---`, so no group write is granted on the directory. A failure to set the receipt's mode is swallowed and the spawn continues. **Build at the merged HEAD:** `Tests run: 1464, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`, exit 0. Run unpiped to a file. **The thing I checked before merging, because a pre-created file is a good way to silence the alarm it exists to raise.** The receipt now always exists, so "the file is there" can no longer mean "the scrub ran". I read `readReport` rather than assuming: it returns `null` when the file is empty and when the first line does not start with `allowed `. So an untouched pre-created receipt still produces "no measurement available", and `releaseZdotdir` still warns. No regression. Anyone reading a receipt by hand needs to know this — an empty file means the scrub did **not** run. **Mutation, on the half the worker did not pin.** The worker proved its own line by deleting the pre-created receipt. So I mutated the other branch instead: the `else` arm that keeps every non-receipt file at `rw-r-----`, changed to `rw-rw----`. That is the security half — the reason the directory does not get group write in the first place. It was caught, by three tests that were already there: ``` EnvAllowListScrubTest.shareWithGroupCoversEveryFlatFileIncludingTheOptionalOnes:171 opencode.json must be group-readable, never group-writable ==> expected: <rw-r-----> but was: <rw-rw----> ClaudeCodeLauncherTest.memberHerdrSocketWithWorktreeRootAndGroupPutsCharterUnderWorktreeRootAndSharesIt:2185 the charter file must be group-readable, never group-writable ==> expected: <rw-r-----> but was: <rw-rw----> OpenCodeLauncherTest.memberHerdrSocketWithWorktreeRootAndGroupPutsConfigDirUnderWorktreeRootAndSharesIt:865 ``` That is the outcome I want and do not often get: the invariant was pinned by a test named for its own denominator, covering every flat file including the optional ones. No gap this time. **One small fix on merge:** five new javadoc lines came in with six leading spaces instead of five. Whitespace only, inside a comment. **One thing the change removed that is worth saying out loud.** The old javadoc claimed the scrub's report write "fails closed rather than open". That is no longer true, and the worker correctly deleted the sentence. The receipt is now writable by the member, so it is evidence produced by the thing being checked. That is the deliberate trade this ticket asked for — without it the write always failed and the WARN fired forever — but it means a present, well-formed receipt proves the scrub ran only if you trust the member. It is a receipt, not an attestation. **And the reason this ticket is not the whole story: #388.** #384 fixes the message and the receipt. It does not make the scrub run. On a host whose panes are neither login nor interactive shells, the scrub never executes at all, because it is sourced from `.zshrc` and `.zlogin` and never from `.zshenv` — the one file zsh always reads. Measured on fleet01 over 8 spawns by that host's lead. #388 has the evidence and the proposed fix. So the receipt work here is still worth having: once #388 makes the scrub run everywhere, a present receipt becomes the normal case and a missing one becomes a real signal instead of the constant it is on that host today.
ltms closed this issue 2026-09-10 01:55:28 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#384