Conversation
FM_HOME selects which home's data/, state/, config/, and projects/ a command operates on, so a secondmate process whose FM_HOME named another home operated on that home with full authority. That happened twice: a secondmate spawned a worker into the primary home, and a secondmate's memory sweep read and rewrote the primary home's captain and learning records. fm-spawn's primary-only domain-mate check could not stop the first, because it inspects $FM_HOME - the value that was already wrong - rather than the running process. bin/fm-home-identity-lib.sh is the new owner of home identity and of the refusal. fm-spawn, fm-send, fm-startup-memory-budget, and fm-stow-cascade source it and exit 4 before any spawn, steer, or memory accounting when the selected home is not this process's own. It fails closed on either of two independent signals: the executing code root's own corroborated secondmate identity, which also covers a sibling mate; and the FM_PUBLIC_FOLLOWUP_PRIMARY_HOME session binding, which covers a mate that invokes the primary home's own bin/ by absolute path. An identity marker counts only when the home's durable parent binding and that parent's registry place the same id at the same path, so a marker left behind in a re-leased pool worktree establishes nothing. The refusal is one-way: a primary home keeps reaching the mates it owns, which the stow cascade, backlog handoff, and --secondmate spawns depend on, while the primary home's own data stays read-only from every mate and worker. Also fix a test-hermeticity bug found on the way: the remote-secondmate and cursor-primary fixtures built their fake code roots by tarring the runner's checkout without excluding the gitignored home identity files, so running the suite from any real secondmate home handed that home's identity to the fixture and tripped the pre-existing primary-only guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRrBY9ykN3yGy1N2QbYzHh
…ve fixture in tests/fm-spawn-batch.test.sh to use the selected home’s canonical projects directory. Focused batch and home-identity tests pass, along with bash syntax and diff checks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
CEO overview
What changed technically
The environment variable FM_HOME selects which home's data, state, config, and projects directory a command operates on, and every fm-* entrypoint resolves it identically. A secondmate process whose FM_HOME named a different home therefore operated on that home with full authority. The existing primary-only domain-mate check in bin/fm-spawn.sh could not stop the spawn incident, because it inspects $FM_HOME, which is the value that was already wrong, rather than the running process.
New library bin/fm-home-identity-lib.sh owns the refusal and how a home's identity is read for it; bin/fm-home-seed.sh still owns writing the marker. Guarded entrypoints are bin/fm-spawn.sh for every spawn kind including --secondmate, bin/fm-startup-memory-budget.sh and bin/fm-stow-cascade.sh for stow memory operations, and bin/fm-send.sh for steering. Each refuses with exit status 4 after FM_HOME is resolved and before anything is read for a write, spawned into, or steered, including before any library that would create the target's state directory.
Accepted scope, settled during review: this is an accidental-misrouting guard, not process provenance or impersonation protection, and the library's LIMIT section states that plainly. A process can unset or alter its inherited environment, leaving only code-root protection. Remotely placed homes run from their host's separate tracked code root, which carries no identity marker, so this guard neither establishes a remote session's own-home provenance nor protects a same-host sibling from it. Use of the primary home's own scripts by absolute path gets primary-only containment rather than sibling protection. The identity marker is a routing convention and never authorization on its own.
Two signals refuse, either alone. The code-root signal is the identity of the home whose bin directory is executing, resolved from the library's own physical location so FM_ROOT_OVERRIDE cannot relabel it; it covers a mate reaching the primary home and is the only signal covering a mate reaching a sibling. It counts only when corroborated: the code root must carry the .fm-secondmate-parent binding whose local parent registers that id at that exact path in data/secondmates.md. That requirement exists because the marker is gitignored and a pooled task worktree can be re-leased from a retired home with its marker still present; a retired marker is registered nowhere and establishes nothing. The launch-binding signal uses FM_PUBLIC_FOLLOWUP_PRIMARY_HOME, already stamped into every secondmate session, to contain accidental selection of the primary home through the primary's own scripts.
Directory overrides are covered too, since FM_STATE_OVERRIDE and its siblings reach the same directories FM_HOME selects. An override is refused only when it resolves inside another registry-corroborated home; an unrelated explicit directory keeps working, which preserves the alternate-directory capability the test suite exercises. Identity validation accepts exactly the character and shape contract the shared registry parser accepts, with no stricter local policy, and rejects an unsafe marker - a symlink, a directory, an empty file, a second line, or NUL bytes - rather than silently treating it as the unmarked primary.
The refusal is one-way, which protects the primary home's records from ordinary mate and worker execution while keeping deliberate correct selections working. Neither signal fires when the executing home is the primary, so the memory cascade running each mate's own accounting, backlog handoff into a mate's queue, and standing a mate up all continue unchanged. A home operating on itself is always allowed. Refusal diagnostics name the resolved identities and canonical paths involved and carry no credential or message content.
Documentation is updated without duplicating the contract: the library header owns it, docs/architecture.md gains a cross-home boundary section beside the existing gate boundary, docs/configuration.md and the four entrypoint help surfaces carry pointers, and the stow and secondmate-provisioning skills carry their trigger lines. A pre-existing truncation in the startup-memory help output is fixed in the same pass.
Also fixed, found while validating: four test fixtures built their fake code roots by copying the runner's checkout without excluding the gitignored home identity files, so running the suite from a real secondmate home handed that home's identity to the fixture.
Validation
Module-boundary decision
Current module retained: the four entrypoints keep their existing boundaries and the refusal lives in one shared helper library beside the existing gate-refusal helper, following that established pattern, so the contract is stated once and each entrypoint carries only a call and a pointer.
Decision needed
No decision required.
What Changed
Risk Assessment
Testing
Live CLI validation in fresh isolated homes exercised the guard and its side-effect boundaries, with the documented test-harness gate bypass used only to reach the guard; the focused behavioral suite corroborated the results. This is a CLI-only change, so rendered UI evidence does not apply.
fm-sendexited 4 withcode-root; no primary inbox was created.--secondmatespawns exited 4; no metadata, task data, or absent state directory was created.role=secondmate.fm-stow-cascadecompleted registered mate accounting.launch-bindingbefore memory output.surface-override; no sibling inbox was created.Evidence: Live cross-home CLI transcript
Evidence: Targeted behavioral regression output
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 4 issues found → auto-fixed (12) ✅
bin/fm-send.sh:210- Intent requires that “One agent home can no longer reach into another agent's home.” The new guard receives only$FM_HOME, butSTATEmay instead beFM_STATE_OVERRIDE: a mate can run its ownfm-sendwithFM_HOME=/mateandFM_STATE_OVERRIDE=/primary/state; the guard permits its own home, then writes the primary task inbox viaSTATE. The same override pattern reaches cross-home state/data/config in the other guarded interfaces. Decide whether overrides must be constrained to the selected home; without that, the claimed boundary remains bypassable.bin/fm-home-identity-lib.sh:232- Intent says the guard prevents one mate reaching another. In a secondmate session, invoking the primary bin by absolute path withFM_HOME=/siblingleaves the code-root signal as primary and the launch-binding check false because it rejects only when the target equals the primary binding.fm-sendthen operates on the sibling state. Covering this path needs a trusted binding for the invoking mate's own home or an explicitly authorized restriction.bin/fm-home-identity-lib.sh:221- The stated criterion is that a home's identity is trusted only when the registry confirms it. The code-root check compares only IDs: a registeredmate-acan select a distinct pooled/copy home containing an uncorroborated stalemate-amarker, pass the equality check, and operate on that other home's records. Require canonical home-path equality for an own-home exception, or corroborate the selected marker as well.bin/fm-home-identity-lib.sh:155- Intent explicitly requires a marker with “more than one line” to be an error. This hunk strips all whitespace from lines 2 onward, somate-a\n\nis accepted instead of refused. Detect any second line, including an empty one, before accepting the marker.🔧 Fix applied.
3 errors still open:
bin/fm-home-identity-lib.sh:279- The new launch-binding check also runs after a local code root has already established that the caller selected its own home. An already-running local secondmate launched before this commit has only the pre-existingFM_PUBLIC_FOLLOWUP_PRIMARY_HOME, so its ownfm-send/spawn/stow call now fails the missingFM_PUBLIC_FOLLOWUP_SECONDMATE_HOMEcorroboration with exit 4. Fresh remote secondmates fail too: their seeded parent record isroute=remote, which this local-only corroboration rejects. This contradicts the required behavior that mates keep running their own workers and memory sweeps. Limit this extra binding requirement to the primary-code-root gap, and handle valid remote routes without refusing own-home operations.bin/fm-home-identity-lib.sh:312-FM_HOME_IDENTITY_BYPASS=1disables every signal before the guarded entrypoints operate. For example, a secondmate can run its ownbin/fm-send.shwith that variable andFM_HOMEplusFM_STATE_OVERRIDEpointing at the primary, then write the primary task inbox. The added regression test explicitly confirms this bypass. This contradicts the required fail-closed cross-home boundary; remove this operational environment escape hatch or authorize a test-only mechanism that cannot be inherited by agent commands.bin/fm-home-identity-lib.sh:155- The marker reader captures its first line through shell command substitution, which strips NUL bytes. A marker whose bytes aremate\0-a\nbecomesmate-aand passes the character check, rather than being rejected as an id outside the registry character set. Reject NUL-bearing marker files before extracting the id, as the existing parent-binding parser does.🔧 Fix applied.
3 issues (2 errors, 1 warning) still open:
bin/fm-home-identity-lib.sh:319- The new surface-override check blocks the existing remote secondmate launch path.fm-remote-secondmate-control.shinvokesfm-spawn.shwithFM_HOME=$FM_ROOTbut deliberately stores its endpoint state/data under the selected remote mate ($TARGET_HOME/state/parent-routeand$TARGET_HOME/data/.parent-route); this check rejects those paths as cross-home before--secondmateis handled. Preserve this established, captain-required primary-to-mate startup path with a narrowly authenticated internal exception rather than treating its control records as arbitrary overrides.bin/fm-home-identity-lib.sh:281- The launch-binding signal is optional and entirely inherited environment state. A secondmate using the primary bin by absolute path can runenv -u FM_PUBLIC_FOLLOWUP_PRIMARY_HOME -u FM_PUBLIC_FOLLOWUP_SECONDMATE_HOME FM_HOME=<primary> <primary>/bin/fm-send.sh ...; code-root then identifies the primary and this branch is skipped, allowing a write to the primary task inbox. This contradicts the required criterion that the main home be read-only to every mate. A real fix needs provenance that the invoking agent cannot unset or forge, which expands beyond the accepted environment-binding mechanism and needs a captain decision.docs/architecture.md:220- The new architecture section says this helper's header owns a “test-harness bypass,” but the current fix round removed that bypass from both the helper andtests/lib.sh. Remove the stale reference so operators are not directed to a nonexistent boundary exception.🔧 Fix applied.
2 errors still open:
bin/fm-home-identity-lib.sh:316- The required rollout compatibility is still broken for an already-running remote secondmate. The pre-change launch environment stamped onlyFM_PUBLIC_FOLLOWUP_PRIMARY_HOME; after this update, its commands run from the remote tracked code root (which has no marker), select its own remote home, and reach line 316 with the new own-home binding absent. That path exits 4 beforefm-send, spawn, or stow can operate. This contradicts the required intent that a mate still run its own workers and memory sweep as it picks up this version. Preserving both legacy remote own-home operation and sibling refusal needs an approved provenance/compatibility rule.bin/fm-home-identity-lib.sh:308- The launch binding can still be stripped by the invoking mate. A secondmate can runenv -u FM_PUBLIC_FOLLOWUP_PRIMARY_HOME -u FM_PUBLIC_FOLLOWUP_SECONDMATE_HOME FM_HOME=<primary> <primary>/bin/fm-send.sh ...; the primary code root has no secondmate identity, line 308 skips the only remaining signal, and fm-send then writes the primary state inbox. The same path permits the protected memory reads through the guarded primary scripts. This contradicts the intent that the main home is read-only to every mate and worker. A durable fix requires trusted caller provenance beyond inherited environment variables, so the mechanism needs authorization.🔧 Fix applied.
1 error still open:
bin/fm-spawn.sh:325- The required refusal is not before every write: the new guard at line 325 runs afterfm-wake-lib.shwas sourced at line 258, and that library immediately executesmkdir -p "$STATE". A registered mate running its ownbin/fm-spawn.shwithFM_HOME=<primary whose state/ is absent>therefore creates<primary>/statebefore exiting 4. This contradicts the intent criterion that each entrypoint refuse “before anything is written”; move the refusal ahead of the wake-library source.🔧 Fix applied.
2 errors still open:
bin/fm-home-identity-lib.sh:298- Required criterion says the code-root signal is the sibling-mate protection, but a remote mate has no corroborated code-root identity:fm_home_identity_corroborated_idrejects itsroute=remoteparent, and this launch-binding branch rejects only the remote code root. An unmodified remote mate can setFM_HOMEto a same-host sibling's home and run its ownbin/fm-send.sh, which then writes the sibling inbox. This is distinct from the documented env-unset limitation. Preventing it needs retained authenticated own-home provenance for remote agent sessions, beyond the accepted containment mechanism.bin/fm-home-identity-lib.sh:118- The intent requires rejecting ids outside the registry's own[A-Za-z0-9._-]character set. This added validator also rejects any leading dot and ids longer than 128 characters, although the shared registry parser accepts both. A registry-confirmed home with such an id is therefore refused even for its own operations. Align this predicate with the registry contract, or explicitly authorize the narrower identity-id policy.🔧 Fix applied.
✅ Re-checked - no issues remain.
bin/fm-home-identity-lib.sh:233- The new surface-override branch refuses any override not exactly equal to$FM_HOME/<surface>, including a primary using a separate non-home projects directory. Before this change,tests/fm-spawn-batch.test.shexercisedFM_HOME=<temp home> FM_PROJECTS_OVERRIDE=<separate temp projects>and verified normal project resolution; this branch changes the fixture to<home>/projectsto avoid the refusal. That removes the documented alternate-directory capability for tests and specialized harnesses, contradicting the intent's “Work that was already correct is untouched” and “Nothing is removed” criteria. Decide whether overrides are now intentionally canonical-home-only, or preserve safe alternate directories while refusing overrides that resolve to another protected home.🔧 Fix applied.
1 error still open:
bin/fm-home-identity-lib.sh:208- Intent requires that a home's identity is trusted only when its fleet registry confirms it and that a stale marker cannot cause a false refusal. This new helper accepts any readable.fm-secondmate-homeplusroute=remoteparent binding as a protected home without registry corroboration. A primary using a valid separateFM_DATA_OVERRIDE=/tmp/recycled/data, where/tmp/recycledmerely retains a retired remote marker and parent record, is refused byfm-startup-memory-budget.sheven though the override is unrelated to an active home. Decide whether a remote marker alone is now sufficient authority for override blocking, or retain the stated registry-only rule; the narrower intent-compliant remedy is to avoid treating uncorroborated remote markers as protected override targets.🔧 Fix applied.
2 errors still open:
bin/fm-home-identity-lib.sh:232- A normal local mate session can still access the primary through an override when it invokes the primary bin by absolute path.fm-spawnstampsFM_PUBLIC_FOLLOWUP_PRIMARY_HOME=<primary>while retainingFM_HOME=<mate>; withFM_STATE_OVERRIDE=<primary>/state, this code checks the mate's data registry at line 232, does not recognize the primary surface, and returns success.fm-sendthen writes the primary task inbox. The analogous data/config overrides let startup-memory accounting read the primary's preferences and learnings. Extend protected-surface recognition to the corroborated launch-bound primary when code-root identity is unavailable.bin/fm-home-identity-lib.sh:278- Required intent says: “Diagnostics name both home paths, both identities, and which signal fired.” For a surface-override refusal, this new reason names only the override and selected paths/surface, not either home identity. For example, mate-a selecting sibling/state produces nomate-aor sibling id. The generic second line also lacks them. The changed hunk at this line therefore contradicts that required diagnostic contract; approve adding identity resolution to this diagnostic.🔧 Fix applied.
2 errors still open:
bin/fm-home-identity-lib.sh:235- A launch-bound local mate can still steer or spawn into a sibling through an override when it invokes the primary bin by absolute path. With FM_HOME=mate-a, FM_PUBLIC_FOLLOWUP_PRIMARY_HOME=primary, and FM_STATE_OVERRIDE=sibling/state, code-root identity is primary, the binding only checks primary/state, and the probe consults mate-a/data/secondmates.md rather than the bound primary registry. fm-send then writes sibling's inbox without refusing. Use the bound primary as the registry/expected parent when code-root provenance is unavailable but the launch binding is present.bin/fm-home-identity-lib.sh:286- The required diagnostic criterion says refusals name both home paths and identities. A surface-override refusal only reports the selected path and override path; it omits both identities and does not render the protected home's root path. For example, mate-a selecting sibling/state produces neither mate-a nor mate-b. Add resolved selected/protected identities and both canonical home paths to this diagnostic.🔧 Fix applied.
4 errors still open:
bin/fm-home-identity-lib.sh:177-primaryis valid under the registry ID regex but is also used as the unmarked-home sentinel. A locally registered mate whose marker containsprimaryis never corroborated at this check; after its inherited launch binding is unset, it can select the real primary home and steer/spawn there. Track marker presence separately from its ID, or reserveprimaryin the registry contract.bin/fm-home-identity-lib.sh:217- The remote-route check does not return when the parsed parent route is local. In theifcaller, a local parent binding paired with a matching remote registry row passes the later checks, activates the remote-control exception, and skips state/data/config surface protection. Make this predicate fail immediately unlessroute=remote.bin/fm-startup-memory-budget.sh:30- Intent requires that the startup-memory help-range truncation be fixed. The changed range ends at line 14, but the header continues through line 15, so--helpstill omits “owns that contract and its limitations.” Confirm the intended help boundary before changing it.bin/fm-home-identity-lib.sh:383- Intent requires diagnostics to name both home paths and identities. The launch-binding refusal reports only the bound primary path and no invoking-home identity - precisely the absolute-primary-bin route this signal covers. Meeting that requirement needs retained invoking-home provenance, or an explicit narrowing of the diagnostic criterion.🔧 Fix applied.
1 error still open:
bin/fm-home-identity-lib.sh:390- Captain, the absolute-primary-bin route still permits a mate to operate on a sibling by directly selecting it: a spawned mate hasFM_PUBLIC_FOLLOWUP_PRIMARY_HOME=<primary>, invokes<primary>/bin/fm-send.sh(or spawn/memory/stow) withFM_HOME=<sibling>, and uses no surface override. At line 390 the launch-binding signal refuses only when the selected home equals the bound primary; the primary code-root is unmarked, so line 377 establishes no mate origin and the sibling operation proceeds. This contradicts the required criterion that “One agent home can no longer reach into another agent's home.” The current binding cannot distinguish the invoking mate from a sibling, so a durable origin binding or an explicit narrowing to primary-only containment is needed; authorize which contract to preserve.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
bash tests/fm-home-identity.test.shIsolated live CLI drive offm-startup-memory-budget.sh,fm-send.sh,fm-spawn.sh(crewmate and--secondmate), andfm-stow-cascade.shfrom primary, mate, sibling, and recycled-marker homesIsolated livefm-startup-memory-budget.sh --helpcheck✅ No issues found.
fm-sendexited 4 withcode-root; no primary inbox was created.--secondmatespawns exited 4; no metadata, task data, or absent state directory was created.role=secondmate.fm-stow-cascadecompleted registered mate accounting.launch-bindingbefore memory output.surface-override; no sibling inbox was created.bin/fm-session-start.shFresh isolated copied-home live CLI fixture:fm-send.sh,fm-spawn.sh,fm-spawn.sh --secondmate,fm-startup-memory-budget.sh, andfm-stow-cascade.shFM_GATE_REFUSE_BYPASS=1 bash tests/fm-home-identity.test.sh✅ **Document** - passed
✅ No issues found.
✅ No issues found.
✅ No issues found.
🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.
✅ No issues found.