Run members on a second herdr under a dedicated user (optional; single-herdr stays the default) #185
Open
opened 2026-08-28 04:05:56 +02:00 by ltms
·
3 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#185
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up to #184. A member runs as the operator's OS user, so it reads the operator's forge ssh key — a readable, passphrase-free file. Env scrubbing cannot fix that. This ticket is the fix that can: run member panes under a different uid.
Measured end to end on fleet01 on 2026-08-28. The host was restored to its original state afterwards.
The one-line answer
fleetd never forks a process. It sends
{cwd, env, argv}to herdr, and herdr forks the PTY as its own uid. herdr's socket API has no user/uid/run-as parameter onworkspace.create,tab.create,pane.splitoragent.start— confirmed in herdr's own Socket API docs, not only in our client. So the only way to change a member's uid is a second herdr server running as that user.What herdr supports (docs)
Socket path resolution order:
--session <name>→HERDR_SOCKET_PATH→HERDR_SESSION→ default~/.config/herdr/herdr.sock. Named sessions live at~/.config/herdr/sessions/<name>/herdr.sock. fleet01 already runs a named session (fleet01).fleetd already has the knob:
FleetConfig.herdrSocket, plus aHERDR_SOCKET_PATHoverride inUnixSocketHerdrClient.defaultSocketPath().Measured on fleet01 (herdr 0.8.0)
E1 — two servers, two users, two sockets, at once. PASS.
Each server also mints a
-client.sockbeside its api socket.E2 — the socket is mode 0600. This is a real blocker.
umask 007before starting the server does not change it (verified with a third server) — herdr sets 0600 explicitly. No config key for socket mode was found. So the member herdr's start unit mustchmod g+rwafter the socket appears, which is a race window and an operational wart. Worth asking herdr upstream for a socket-mode or socket-group option.E3 — the isolation works. This is the #184 fix. PASS.
And the pane really is that user —
herdr pane readon server B returned the promptfleetmbr@fleet01:~$, withcwd: /home/fleetmbr.E4 — id collision is guaranteed, not merely possible. This is the biggest code consequence.
Workspace, tab and pane ids are per-daemon sequential counters. Both servers were live with the same ids at the same moment:
terminal_idis different in shape — a timestamp prefix plus random suffix (term_65a11bc2731862vsterm_65a11c25eba401) — and stayed distinct. But that is probabilistic, not structural, and the prefix collides for panes created in the same second.fleet_stop{paneId}takes a paneId. Under two daemonsw1:p1is ambiguous. Pane ids appear in the MCP surface and in the roster, so they must become daemon-qualified.Design
Not two global clients — a router keyed by target.
Fleetd.java:151builds oneUnixSocketHerdrClientand handsagents+spacesto about fifteen consumers, which split by who they act on:LeadTabScanner(identity),ReplyPushLoop,LeadHeartbeatLoop,LeadCoordLoop,LeadLauncherHerdrPeerLauncher+ both launchers,StatusPoller,StatusRefiner,CompletionResolver,PaneLocator,FleetHealthMonitorInjector,MessageService,FleetAppSingle-herdr stays the default. A new optional key:
Absent means the router returns one client for every target, so behaviour is byte-identical to today. The Mac runs that way. Two-herdr mode is opt-in per host.
Risks, ordered
LeadTabScanner.scan()runsworkspace.list+tab.list+pane.liston one client. It must be pinned to the lead's herdr. Point it at the member herdr and the lead is demoted to worker, and every orchestration call is refused.terminal_identropy.GitWorktreesshellsgit worktree addviaProcessBuilder, so it runs as fleetd's uid and creates the tree owned by the operator. The member then writes there as another user, and committing writes into the operator's.git/objectsand.git/refsregardless. Needs a shared group and setgid. This isolates credentials, not the repo — say so plainly rather than implying more.HerdrPeerLauncher.hostEnvNamesreads fleetd's own environment and assumes the pane's login shell exports the same set. Under a second user that is false, so CB-596's gap detector reports on the wrong environment.fleetworkspace cannot span two daemons. Only affects two-herdr mode; members get their own workspace there.The payoff, beyond #184
Today the member's login shell re-sources the operator's
secrets.sh, which is the whole reasonmemberCredentialsexists. A second user has nothing to source.tab.create{env}becomes the only path a credential can reach a member — the spawn boundary we kept trying to build inside a sourced file. The scrub stops being the control and becomes defence in depth.Staging
memberHerdrSocket(absent = today). Qualify pane ids. PinLeadTabScanner.hostEnvNamesfor the two-user case.Related
sshAuthSock: blockdecision whose premise #184 disproved.Must be fixed BEFORE
memberHerdrSocketis ever set — running listTwo PRs have now landed or been reviewed for this epic, and both had the same shape of defect:
correct today only because both herdr clients are the same object, wrong the moment the second
daemon is configured. Every test passed in each case. Keeping the list in one place so nothing
enables the feature on top of a half-routed daemon.
Landed
stop()/list()pane-id ambiguity. Merged asa237fbf. Includes three lead-reviewfixes:
stop()no longer forgets the owner when the delegate refuses,list()is keyed by(owning daemon, pane id) instead of the raw pane id, and the class javadoc no longer states the
single-connection premise as fact. 993 tests green.
Open, blocking
#186 — three unrouted consumers. Sent back to a member. In short:
PaneLocatoris pinned to the member daemon, so a lead's connection resolves to no terminal.FleetMcp.replyandFleetMcp.askboth refuse whencallerTerminal == null, so a lead can nolonger answer a peer lead, and
fleet_whoamiloses its leader name.StatusRefineris pinned to the member daemon while the status call next to it is routed pertarget. A lead whose raw status reads UNKNOWN gets refined against the wrong daemon and stays
UNKNOWN, which wedges status-gated delivery to that lead.
FleetAppnever goes through the router:/healthzstays green while the MEMBER daemon isdown (and then every spawn fails), and
GET /sessionsdrops every member workspace.Pane ownership does not survive a restart.
CompositePeerLauncher.spawnedByis in memory. Withtwo daemons, every pane that outlives the daemon is unowned, so
fleet_stopon it refuses — bydesign, since guessing would close a pane on an arbitrary daemon. Recovering ownership at boot is
its own unit and it is a prerequisite, not a nicety.
The socket is mode 0600. Cross-user access needs a post-start
chmod g+rw, andumaskdoesnot change it. Measured on herdr 0.8.0. An upstream request is drafted; until then any start unit
has to poll for the socket and relax it, which is a race on every start.
Lower severity, do not lose
HerdrPeerLauncher.stophas the same before-the-call removal that #187 just fixed one level up:paneByAgentId.remove(idOrPane)runs beforeagents.close(paneId). A failed stop there falls backto raw-pane addressing rather than refusing, so it degrades instead of breaking.
The pattern worth naming
Three of these were found by asking one question of each consumer: which daemon does this call go
to, and is that the daemon that owns the thing it is asking about? None was found by a test.
A test that builds its own object graph proves the seam works; it says nothing about whether the
production wiring uses it. Any further PR on this epic should assert against the real wiring.
Reopened. PR #196 auto-closed this on merge, but it only cleared the two stage-1 blockers — the ticket has four stages and three are untouched.
Done (merged as
63c19dc):stop()after a restart.spawnedByis in-memory only, so a daemon restart made every surviving member un-stoppable under two daemons.CompositePeerLauncher.probeOwnernow asks each distinct daemon which one knows the pane: one match routes and caches, no match is treated as already-gone, more than one match is a genuine ambiguity (per-daemon counters — E4 above) and is refused with a message that now says so.probeOwnercalledlist()unguarded, so one unreachable daemon made panes on a different healthy daemon un-stoppable too — blocker 1 resurfacing through a new door. Eachlist()is now wrapped per daemon./healthzmasking a broken member daemon. It pinged the member daemon and discarded the answer, so a member herdr with a mismatched protocol looked green while every spawn failed. It now reports the member daemon's version/protocol under a separatememberkey, and setsprotocolMismatch: truewhen the two protocol numbers differ. Theherdrkey still carries the lead daemon's values unchanged, becauseredeploy-fleetd.shandrename-checkout.shboth read this endpoint.Still open — the reason this ticket stays open:
fleet_stop{paneId}still takes a barew1:p1.probeOwnermakes the ambiguous case fail loudly instead of closing the wrong pane, which is a safety fix, not the design fix. The ids in the MCP surface and the roster are still not daemon-qualified.hostEnvNamesstill reads fleetd's own environment (risk 4). Under a second user that is the wrong environment, so CB-596's gap detector reports on a set the member never had. This now interacts with the #192 work merged today: the gap report distinguishes names the scrub blanks from names the derived allow-list keeps, and both halves are computed fromhostEnvNames.memberHerdrSocket:is unset on both hosts, so two-herdr mode has never run outside the fleet01 experiment.Verified by the lead: merged with #194 onto an integration branch off main, full
mvn clean installgreen at 1025 tests.Stage 3 in progress, and four more files that assume fleetd's uid owns everything it writes
Stage 3 (worktree group/setgid provisioning) is implemented in PR #208: an opt-in
worktreeGroup:key, a
Worktrees.shareWithGroupseam, and aSessionManagercall placed afteroverlayParity— because
overlayParitycopies more files into the worktree afteradd()returns, so sharing anyearlier leaves every overlay file operator-owned and unwritable. A test pins that order.
Two defects found in review and fixed before merge, both of which would have failed every
provisioning spawn rather than just the two-user case:
.git/logswas passed tochgrpunguarded. It does not exist whencore.logAllRefUpdates=falseor before the first ref update, and
chgrpon a missing path exits non-zero — surfacing as aWorktreeExceptionblaming a group that is in fact fine. Every path is now skipped when absent.repoRoot + "/.git"was hardcoded. That is a file, not a directory, when the checkout isitself a linked worktree — the very thing this codebase creates for every member. Now resolved
with
git rev-parse --git-common-dir(relative or absolute, resolved againstrepoRoot).The stage-3 fix-up does not reach four other writes
Found while reviewing #208. Reported, not fixed — these belong to stage 4, not to that PR.
1. The role charter goes to the system temp directory, which on macOS is per-user 0700.
ClaudeCodeLauncher.writeCharterFile(ClaudeCodeLauncher.java:454) doesFiles.createTempFile("fleetd-role-charter-", ".md")and passes the path to the member as a launchflag. That path is outside the worktree, so
worktreeGroupcannot reach it. I checked what thedirectory actually is:
0700, owned by the operator. A member running as a different user cannot even traverse it, soit never reads its charter — and the charter is what carries the reply contract. This is a hard
blocker for two-user mode, not a permissions nicety. The same applies to
EnvAllowListScrub.write(EnvAllowListScrub.java:103), which creates itsZDOTDIRwithFiles.createTempDirectory(parentDir, …): the member's own login shell, as another uid, has to readwhat is generated there.
2. Two launcher writes land after the fix-up has already run.
ClaudeCodeLauncher.writeIdeOverlay(CLAUDE.local.mdinto the worktree root) and its companionwrite to
<commonDir>/info/excludeboth happen at spawn time, i.e. afterSessionManagerhas calledshareWithGroup. Those files are fleetd-uid-owned with onlyumask-derived group access. They are read-only from the member's side today, so
0644happens to beenough — but nothing states that, and the moment one becomes member-writable it breaks silently.
This is the same ordering trap as
overlayParity, one layer further out: the permission pass hasto be the last thing that touches the tree, and spawn is after it.
3. Not checked.
OpenCodeLauncher's equivalent config/charter writes. The shape is likely thesame.
What this means for staging
Stage 3 alone does not make two-user mode work. Item 1 above has to be solved first, and it is not a
chmod— a per-user0700temp directory cannot be opened up safely. The charter and the scrubdirectory need to move somewhere both uids can reach, most likely under the worktree (which stage 3
already shares) or a purpose-made directory owned by the group.