fix(bin): strip AI co-author trailers from commits by every fleet runtime - #57
Merged
tiago-peixoto merged 11 commits intoSep 18, 2026
Merged
Conversation
Cursor injects the trailer after the typed message, and a per-machine cli-config opt-out is not a fleet contract. Every spawn now gives the pane a commit-msg hook that strips known AI trailers at the commit object.
The review wall-clock left these findings unapplied: the pane-wide hooksPath must resolve orig hooks in the repository git is actually running in, secondmate homes must be git checkouts so the strip cannot be skipped, and the documented --no-verify residual stays an accepted risk rather than a push-side check.
…re two separate causes, and I fixed both. 1. Real product bug (Behavior portable serial 2, test `fm-remote-secondmate-lifecycle-e2e`). A remote secondmate keeps its own AI-trailer strip directory at `state/parent-route/<id>.git-hooks` inside its home. The strip directory is read-only on purpose. Before deleting a home, `remove_firstmate_home` in `bin/fm-teardown.sh` restores write permission on strip directories, but it only matched `state/*.git-hooks`. So retiring a remote secondmate hit "Permission denied" and left a half-deleted home behind. The fix replaces that pattern with a `find` over the whole `state/` folder for `*.git-hooks` directories, so strip directories at any depth get write permission back before the home is removed or returned to treehouse. I ran a copy of the old code: it failed with the same error as CI. The fixed code passes the full test. 2. Test setup never updated for an earlier ruling (Behavior portable serial 4 and 5, Behavior tests (Herdr)). The round-3 ruling made every secondmate launch refuse to start when its home is not a git checkout, and said test homes must become real git checkouts. Four places still built non-git secondmate homes, so the secondmate launch failed with "not a git worktree": `fm-secondmate-liveness` (`add_sm_home`), `fm-session-start` (both the tmux and the Herdr helper), `fm-backend-herdr-launcher-workspace-e2e` (secondmate-home-2) and `fm-backend-herdr-workspace-per-home-e2e`. Each now gets the same two setup lines other tests on this branch already use: a `.gitignore` and `git init -q -b main`. The Herdr helper in `fm-session-start` was not in the CI log, because the test stops at its first failure, but it failed locally once the first helper was fixed. Verification on macOS with git and herdr 0.9.0: I ran the old liveness test and it failed the same way as CI. After the fix, these all pass: `fm-secondmate-liveness`, `fm-session-start` (54 checks), both real-Herdr end-to-end tests, `fm-remote-secondmate-lifecycle-e2e` and `fm-backend-cmux`. Shellcheck gives the same warning count on every changed file as before. One local-only problem remains that this branch did not cause and I did not fix. With the default macOS TMPDIR under `/var/folders` (a symlink to `/private/var`), `fm-remote-secondmate-lifecycle-e2e` fails early with "staged record authorized directory cannot be resolved". Linux CI does not hit it. Setting TMPDIR to a path with no symlink in it avoids it
tiago-peixoto
marked this pull request as ready for review
September 18, 2026 03:45
tiago-peixoto
added a commit
that referenced
this pull request
Sep 18, 2026
…time (#57) * Stop AI co-author trailers from landing on fleet-launched commits Cursor injects the trailer after the typed message, and a per-machine cli-config opt-out is not a fleet contract. Every spawn now gives the pane a commit-msg hook that strips known AI trailers at the commit object. * no-mistakes(review): Fix hooksPath resolution, env leakage, and trailer matching * no-mistakes(review): Chain project hooks at run time, add live Cursor evidence * Fail closed on non-git launches and chain hooks per-repository The review wall-clock left these findings unapplied: the pane-wide hooksPath must resolve orig hooks in the repository git is actually running in, secondmate homes must be git checkouts so the strip cannot be skipped, and the documented --no-verify residual stays an accepted risk rather than a push-side check. * no-mistakes(review): Drop hot-path git hooks and dead hooks-installed flag * no-mistakes(review): Single-owner worktree check, accurate hook and hint notes * no-mistakes(review): Read-only strip dir, exact bot emails, honest hook cost * no-mistakes(review): Clean up leaked read-only strip dirs on abort and teardown * no-mistakes(review): Keep strip for launched agents whose endpoint stayed open * no-mistakes(document): Clarify AI-trailer strip hook wording in configuration docs * no-mistakes(ci): This branch caused all four failing checks. There were two separate causes, and I fixed both. 1. Real product bug (Behavior portable serial 2, test `fm-remote-secondmate-lifecycle-e2e`). A remote secondmate keeps its own AI-trailer strip directory at `state/parent-route/<id>.git-hooks` inside its home. The strip directory is read-only on purpose. Before deleting a home, `remove_firstmate_home` in `bin/fm-teardown.sh` restores write permission on strip directories, but it only matched `state/*.git-hooks`. So retiring a remote secondmate hit "Permission denied" and left a half-deleted home behind. The fix replaces that pattern with a `find` over the whole `state/` folder for `*.git-hooks` directories, so strip directories at any depth get write permission back before the home is removed or returned to treehouse. I ran a copy of the old code: it failed with the same error as CI. The fixed code passes the full test. 2. Test setup never updated for an earlier ruling (Behavior portable serial 4 and 5, Behavior tests (Herdr)). The round-3 ruling made every secondmate launch refuse to start when its home is not a git checkout, and said test homes must become real git checkouts. Four places still built non-git secondmate homes, so the secondmate launch failed with "not a git worktree": `fm-secondmate-liveness` (`add_sm_home`), `fm-session-start` (both the tmux and the Herdr helper), `fm-backend-herdr-launcher-workspace-e2e` (secondmate-home-2) and `fm-backend-herdr-workspace-per-home-e2e`. Each now gets the same two setup lines other tests on this branch already use: a `.gitignore` and `git init -q -b main`. The Herdr helper in `fm-session-start` was not in the CI log, because the test stops at its first failure, but it failed locally once the first helper was fixed. Verification on macOS with git and herdr 0.9.0: I ran the old liveness test and it failed the same way as CI. After the fix, these all pass: `fm-secondmate-liveness`, `fm-session-start` (54 checks), both real-Herdr end-to-end tests, `fm-remote-secondmate-lifecycle-e2e` and `fm-backend-cmux`. Shellcheck gives the same warning count on every changed file as before. One local-only problem remains that this branch did not cause and I did not fix. With the default macOS TMPDIR under `/var/folders` (a symlink to `/private/var`), `fm-remote-secondmate-lifecycle-e2e` fails early with "staged record authorized directory cannot be resolved". Linux CI does not hit it. Setting TMPDIR to a path with no symlink in it avoids it
tiago-peixoto
added a commit
that referenced
this pull request
Sep 21, 2026
…time (#57) * Stop AI co-author trailers from landing on fleet-launched commits Cursor injects the trailer after the typed message, and a per-machine cli-config opt-out is not a fleet contract. Every spawn now gives the pane a commit-msg hook that strips known AI trailers at the commit object. * no-mistakes(review): Fix hooksPath resolution, env leakage, and trailer matching * no-mistakes(review): Chain project hooks at run time, add live Cursor evidence * Fail closed on non-git launches and chain hooks per-repository The review wall-clock left these findings unapplied: the pane-wide hooksPath must resolve orig hooks in the repository git is actually running in, secondmate homes must be git checkouts so the strip cannot be skipped, and the documented --no-verify residual stays an accepted risk rather than a push-side check. * no-mistakes(review): Drop hot-path git hooks and dead hooks-installed flag * no-mistakes(review): Single-owner worktree check, accurate hook and hint notes * no-mistakes(review): Read-only strip dir, exact bot emails, honest hook cost * no-mistakes(review): Clean up leaked read-only strip dirs on abort and teardown * no-mistakes(review): Keep strip for launched agents whose endpoint stayed open * no-mistakes(document): Clarify AI-trailer strip hook wording in configuration docs * no-mistakes(ci): This branch caused all four failing checks. There were two separate causes, and I fixed both. 1. Real product bug (Behavior portable serial 2, test `fm-remote-secondmate-lifecycle-e2e`). A remote secondmate keeps its own AI-trailer strip directory at `state/parent-route/<id>.git-hooks` inside its home. The strip directory is read-only on purpose. Before deleting a home, `remove_firstmate_home` in `bin/fm-teardown.sh` restores write permission on strip directories, but it only matched `state/*.git-hooks`. So retiring a remote secondmate hit "Permission denied" and left a half-deleted home behind. The fix replaces that pattern with a `find` over the whole `state/` folder for `*.git-hooks` directories, so strip directories at any depth get write permission back before the home is removed or returned to treehouse. I ran a copy of the old code: it failed with the same error as CI. The fixed code passes the full test. 2. Test setup never updated for an earlier ruling (Behavior portable serial 4 and 5, Behavior tests (Herdr)). The round-3 ruling made every secondmate launch refuse to start when its home is not a git checkout, and said test homes must become real git checkouts. Four places still built non-git secondmate homes, so the secondmate launch failed with "not a git worktree": `fm-secondmate-liveness` (`add_sm_home`), `fm-session-start` (both the tmux and the Herdr helper), `fm-backend-herdr-launcher-workspace-e2e` (secondmate-home-2) and `fm-backend-herdr-workspace-per-home-e2e`. Each now gets the same two setup lines other tests on this branch already use: a `.gitignore` and `git init -q -b main`. The Herdr helper in `fm-session-start` was not in the CI log, because the test stops at its first failure, but it failed locally once the first helper was fixed. Verification on macOS with git and herdr 0.9.0: I ran the old liveness test and it failed the same way as CI. After the fix, these all pass: `fm-secondmate-liveness`, `fm-session-start` (54 checks), both real-Herdr end-to-end tests, `fm-remote-secondmate-lifecycle-e2e` and `fm-backend-cmux`. Shellcheck gives the same warning count on every changed file as before. One local-only problem remains that this branch did not cause and I did not fix. With the default macOS TMPDIR under `/var/folders` (a symlink to `/private/var`), `fm-remote-secondmate-lifecycle-e2e` fails early with "staged record authorized directory cannot be resolved". Linux CI does not hit it. Setting TMPDIR to a path with no symlink in it avoids it
tiago-peixoto
added a commit
that referenced
this pull request
Sep 21, 2026
…s Pi change. They fail with the same tests on main at the base commit 09dc7b3 (CI run 35613893238). Two tests that failed here but not visibly on main (fm-kimi-harness and fm-spawn-compact-adviser-disable-remote) were in main's shard 9, which stopped at an actionlint download error before any test ran. Two regressions came in when the fork's own commits were rebased onto upstream on Sep 21. As you asked, I kept the previous agent's two partial fixes. I checked each one against the logs and locally, and made no other changes. 1. bin/fm-spawn.sh: restored the short staged launch line. Upstream kunchenguid#4994 (a452a79) writes the full launch command to a private file and types only `. <launch-file>` into the pane, because typed lines over about 1,024 bytes get cut off. The fork's #57 (a32fce8) put the old `spawn_send_literal "$T" "$LAUNCH"` back while resolving a merge, so it typed the whole command again. That broke fm-claude-trust, fm-backend-orca, fm-kimi-harness, fm-spawn-dispatch-profile, both fm-spawn-compact-adviser-disable suites, fm-remote-secondmate-trace-context and fm-remote-secondmate-parent-binding. The fix is one line that restores kunchenguid#4994's `spawn_send_literal "$T" ". $(shell_quote "$LAUNCH_FILE")"` and keeps #57's `SPAWN_LAUNCH_SENT=1`. 2. tests/fm-remote-reply.test.sh: the fixture also resolves the `default` key. Upstream kunchenguid#3764 added two decision lines with no key (`needs-decision [at=...]: which base branch?`), and these count under the key `default`. The fork's #48 made the automatic recovery repost wait while the mate has any open decision, so "the one automatic recovery repost was not sent". I confirmed this by printing the open decisions at that point: only `default needs-decision which base branch?` was open. Only the fixture's setup changed; every assertion is unchanged, and #48's wait-while-open rule still applies. Verification on macOS: all 8 spawn test files and fm-remote-reply pass through bin/fm-test-run.sh. With the spawn line reverted, fm-claude-trust fails with the same message as CI ("the launch command did not carry the brief the worker must read"). bash -n and shellcheck -S warning pass on both files. No Pi code was touched
tiago-peixoto
added a commit
that referenced
this pull request
Sep 21, 2026
* fix(pi): stop nested Pi CLI from replacing a live session binding A short-lived child such as fm-spawn's pi --help probe was treated as lock-owned through ancestry and overwrote both markers with a pid that died immediately, causing false supervision alarms. * no-mistakes(review): Remove duplicate Pi turn-end marker regression tests * no-mistakes(review): Point Pi marker verification doc at remaining regression suite * no-mistakes(review): Test Pi self-lock marker binding; drop dead turn-end ownership code * no-mistakes(ci): These four failing shards are not caused by this PR's Pi change. They fail with the same tests on main at the base commit 09dc7b3 (CI run 35613893238). Two tests that failed here but not visibly on main (fm-kimi-harness and fm-spawn-compact-adviser-disable-remote) were in main's shard 9, which stopped at an actionlint download error before any test ran. Two regressions came in when the fork's own commits were rebased onto upstream on Sep 21. As you asked, I kept the previous agent's two partial fixes. I checked each one against the logs and locally, and made no other changes. 1. bin/fm-spawn.sh: restored the short staged launch line. Upstream kunchenguid#4994 (a452a79) writes the full launch command to a private file and types only `. <launch-file>` into the pane, because typed lines over about 1,024 bytes get cut off. The fork's #57 (a32fce8) put the old `spawn_send_literal "$T" "$LAUNCH"` back while resolving a merge, so it typed the whole command again. That broke fm-claude-trust, fm-backend-orca, fm-kimi-harness, fm-spawn-dispatch-profile, both fm-spawn-compact-adviser-disable suites, fm-remote-secondmate-trace-context and fm-remote-secondmate-parent-binding. The fix is one line that restores kunchenguid#4994's `spawn_send_literal "$T" ". $(shell_quote "$LAUNCH_FILE")"` and keeps #57's `SPAWN_LAUNCH_SENT=1`. 2. tests/fm-remote-reply.test.sh: the fixture also resolves the `default` key. Upstream kunchenguid#3764 added two decision lines with no key (`needs-decision [at=...]: which base branch?`), and these count under the key `default`. The fork's #48 made the automatic recovery repost wait while the mate has any open decision, so "the one automatic recovery repost was not sent". I confirmed this by printing the open decisions at that point: only `default needs-decision which base branch?` was open. Only the fixture's setup changed; every assertion is unchanged, and #48's wait-while-open rule still applies. Verification on macOS: all 8 spawn test files and fm-remote-reply pass through bin/fm-test-run.sh. With the spawn line reverted, fm-claude-trust fails with the same message as CI ("the launch command did not carry the brief the worker must read"). bash -n and shellcheck -S warning pass on both files. No Pi code was touched
tiago-peixoto
added a commit
that referenced
this pull request
Sep 25, 2026
…time (#57) Cursor injects the trailer after the typed message, and a per-machine cli-config opt-out is not a fleet contract. Every spawn now gives the pane a commit-msg hook that strips known AI trailers at the commit object, chaining the repository's own hooks at run time, failing closed on non-git launches, and cleaning up its read-only strip directory on abort and teardown. A secondmate home must be a git checkout so the strip cannot be skipped there; the upstream worker-account fixture is taught that. Fork patch. No upstream thread of this fleet's own tracks it; the nearest upstream threads are other contributors' brief-rule PRs kunchenguid#1379 and kunchenguid#4352 (open) and kunchenguid#4523 (closed by its author as a mistaken target).
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
AI co-author trailers must never reach commits from any non-Claude runtime the fleet launches. Ruled 2026-09-17: "for PR 3764, leave the trailer, it's ok for now. but it shouldn't happen again." Two same-day incidents motivate this: a Cursor co-author trailer reached the replacement commit on mesaTCG PR 55 (head 49fa29489c6c2913e150f7793fafe9b65beee00a) even though the typed commit message was deliberately clean - the trailer was confirmed at the commit object, added by the runtime after the message was written; and a Claude co-author trailer sits on firstmate upstream PR 3764 head 668e47d, which the captain ruled stays (no rewrite of a published head under maintainer review). This task is the recurrence prevention, fleet-wide.
What Changed
bin/fm-git-strip-ai-trailers.sh, a gitcommit-msghook that removes AICo-Authored-Bytrailers from the commit message before git writes the commit. It matches known product names (Cursor, Claude, Copilot, Codex, Gemini, Grok, OpenAI, ...) or exact bot addresses already seen in commits, plusGenerated with ... ClaudeandGenerated-by:lines. Human co-authors are left alone, and so is the author identity.Its
installmode writes a read-only hooks directory. Besidescommit-msg, that directory holds wrappers that run the hooks of whichever repository git is working in, so project hooks such as husky still run.fm-spawn.shinstalls this directory atstate/<id>.git-hooksfor every launch kind and every runtime, Claude included. It points git at the directory by addingGIT_CONFIG_COUNT/KEY_0/VALUE_0(core.hooksPath) to the start of the pane's launch command only, so it never changes the project's git config. If the install fails, the spawn fails. If the spawn aborts, it removes the directory, unless the agent was already launched and its pane or window was not closed.fm-teardown.shremoves the directory for tasks and secondmate children, after first making it writable again.configuration.md,scripts.md,AGENTS.md, Cursor harness reference) record the contract. They also list the gaps the owner accepted:git commit --no-verifyskips the hook, aMade with <product>line is not matched, and hook managers cannot install from inside a fleet pane. A newtests/fm-git-strip-ai-trailers.test.shcovers the hook. Spawn and harness tests now assert the launch command's new prefix, and a sharedfm_test_remove_treeintests/lib.shcleans up fixtures that contain the read-only directory.🤖 Generated with Claude Code
Risk Assessment
Testing
I ran the change's own test file (
tests/fm-git-strip-ai-trailers.test.sh) and the two changed spawn test files,fm-kimi-harnessandfm-spawn-dispatch-profile; all pass. I then drove the real product live on private tmux servers with temp firstmate homes and temp treehouse pools. First, a Cursor crewmate spawned throughfm-spawnwith attribution on (through a temp CURSOR_CONFIG_DIR, so the user's Cursor config was never touched), run on both the base commit and the change: the trailer landed before the change and was stripped after it. Second, a secondmate pane committing in a managed clone. Third, hook-manager-style writes into the strip dir from inside that pane. Fourth, a secondmate spawn whose home is not a git checkout, which was refused. Last, teardown of a crewmate and of a secondmate whose home held a leaked read-only strip dir. Every driven scenario passed. Live Codex stopped at its first-run trust prompt, which would write to~/.codex/config.toml, so it is untested. The kimi open-endpoint abort case is covered only by its stub-based test. All temp labs, tmux servers and extracted trees were removed, and the worktree is clean.git commit --trailer "Co-authored-by: Cursor <cursoragent@cursor.com>" -m ...;git cat-file commit HEADshows only `docs: add strip prob…Co-authored-by: Cursor <cursoragent@cursor.com>; the same --trailer command appears in live-cursor-before/cursor-pane.txt; no strip dir and…project commit-msg hook ran (chained): yes, and the message it saw contains no trailerCo-authored-by: Jane Doe <jane@example.com>, andmanaged-project pre-commit ran in .../projects/managed(real fm-spawn --secondm…error: could not install the AI-trailer strip hooks for sm-nogit-probe; no state record and no launch sent (an empty tmux window is left, se…fm-teardown.sh strip-probe-after --forceexits 0 and the dir is gonefm-teardown.sh sm-strip-probe --forceexits 0; the secondmate home and the parent-side strip dir are both goneCo-authored-by: Codex <noreply@openai.com>trailerEvidence: Before/after live Cursor commit objects summary
Source: Before/after live Cursor commit objects summary
Evidence: Live Cursor fm-spawn transcript with the change (strip dir, commit object, chained project hook)
Source: Live Cursor fm-spawn transcript with the change (strip dir, commit object, chained project hook)
Evidence: Live Cursor pane capture with the change (shows the injected --trailer command)
Source: Live Cursor pane capture with the change (shows the injected --trailer command)
Evidence: Live Cursor fm-spawn transcript on base commit (trailer lands on commit object)
Source: Live Cursor fm-spawn transcript on base commit (trailer lands on commit object)
Evidence: Live Cursor pane capture on base commit
Source: Live Cursor pane capture on base commit
Evidence: Crewmate teardown removes the read-only strip dir
Source: Crewmate teardown removes the read-only strip dir
Evidence: Secondmate pane: managed-clone commit stripped, human kept, clone's own hook ran; non-git home refused
Source: Secondmate pane: managed-clone commit stripped, human kept, clone's own hook ran; non-git home refused
Evidence: Secondmate pane: hook-manager writes refused, next commit still stripped
Source: Secondmate pane: hook-manager writes refused, next commit still stripped
Evidence: Secondmate teardown with a leaked read-only child strip dir
Source: Secondmate teardown with a leaked read-only child strip dir
Evidence: Codex first-run trust prompt that blocked the live Codex scenario
Source: Codex first-run trust prompt that blocked the live Codex scenario
Pipeline
Updates from git push no-mistakes
... (10 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)
🔧 **Review** - 2 issues found → auto-fixed (5) ✅
Sequence: a Cursor crewmate in a lefthook project whose lefthook.yml has a
commit-msgentry (the usual commitlint setup) and no core.hooksPath config runspnpm installin its fresh worktree. The strip's commit-msg becomes commit-msg.old. From then on,git commitruns only lefthook's commit-msg, and the Cursor trailer reaches the commit object with no message. That is the PR 55 failure again.The same root cause breaks
pre-commit install(the pre-commit framework'shas_core_hookpaths_setrunsgit config core.hooksPath). It refuses with "Cowardly refusing to install hooks withcore.hooksPathset", and its unset-config hint is a no-op here.None of the currently registered projects hit the strip hole. artemis sets a local core.hooksPath, so lefthook 2.1.4 refuses there, and central-do-frete uses husky, which only writes local config. But this is ordinary worker behavior for any lefthook+commitlint project.
The remedy is what needs your ruling, not the defect. Options:
(a) Record it as an accepted residual next to --no-verify.
(b) Make the directory read-only after install (with a chmod before the two
rm -rfsites), so managers fail instead of displacing the strip. The cost is that those managers cannot install their hooks from inside panes.(c) Also run the strip from a second hook name (e.g. prepare-commit-msg), so displacing one name does not disable it.
bin/fm-git-strip-ai-trailers.sh:177- The exclusion rationale says reference-transaction and post-index-change "are the only documented names git invokes more than once per command". That is not true. The last round's ruling kept every other name on the premise that each costs "at most one extra fork per git command". But git's sequencer runs prepare-commit-msg and post-commit once for every commit it replays ingit rebaseandgit cherry-pick A..B, andgit amruns the applypatch hooks once per patch.Measured on this machine (git 2.50.1), using this change's installer with no project hooks present:
git commit -amwent from about 107ms to about 530ms, because four wrappers fire per commit and each forks bash, basename, and up to two git processes.The per-invocation cost is the wrapper process, so it can be reduced but not removed. Dropping more names has a real cost too: post-commit, post-checkout, post-merge and pre-push are the names git-lfs installs.
This needs a ruling: accept the cost and correct the comment to state it, or cut the wrapper's per-call cost (see the related simplification finding).
bin/fm-git-strip-ai-trailers.sh:153- Simplification: after the unset, the wrapper resolves the previous hooks directory in three steps:git config --path --get core.hooksPath, a fallback togit rev-parse --git-path hooks, and a manual$PWDjoin for relative results. A singlegit rev-parse --path-format=absolute --git-path hooksalready does all of that. On git 2.50.1 it returned the configured core.hooksPath (a relative.husky/_resolved against the worktree root even when run from a subdirectory, and~/myhooksexpanded to the home directory). It falls back to the common dir's hooks for a linked worktree, and it always returns an absolute physical path, which suits the= "$ours"guard becauseourscomes frompwd -P. The repo already relies on--path-format=absolutein fm-spawn.sh, fm-ff-lib.sh and fm-wake-lib.sh, so this adds no new version requirement. Replacing lines 153-160 with that one call, andname=$(basename "$0")withname=${0##*/}, removes one git fork and one basename fork per wrapper call, which matters given how often these wrappers fire (see per-commit-hooks-multiply-cost).bin/fm-git-strip-ai-trailers.sh:100- The script says "Human Co-Authored-By trailers are left untouched", but the email rule*@cursor.com | *@anysphere.com | *@anthropic.commatches any address at those domains. So a human co-author who works there (for exampleCo-authored-by: Jane Doe <jane@anthropic.com>on a contribution to a vendor repo) is deleted from the commit with no message. The AI identities actually seen are narrower. Claude usesnoreply@anthropic.com, and Cursor'scursoragent@cursor.comis already caught by the separatecursoragent@*rule. I also checked the installed Codex 0.154.0, which emitsCo-authored-by: Codex <noreply@openai.com>, already listed. Recommended narrower form: exact bot addresses instead of whole-domain wildcards. Flagged for your decision because narrowing drops coverage for any unseen vendor bot address the wildcard was meant to catch.🔧 Fix applied.
1 warning still open:
bin/fm-teardown.sh:2485- The last fix round made state/<id>.git-hooks read-only (dir 500, files 500). It restored the write bit only at the two per-id rm -rf sites (bin/fm-teardown.sh:3196 and :3588). The strip script header says "Whoever removes the directory restores the owner write bit first", but the whole-home removal does not do that. remove_firstmate_home calls safe_rm_rf on the secondmate home (bin/fm-teardown.sh:2485 -> :2398, a plainrm -rf -- "$target").A directory can be left behind without any meta record to clean it up. fm-spawn installs the strip at bin/fm-spawn.sh:4174, before the meta record is published. spawn_abort_cleanup (bin/fm-spawn.sh:1118) never removes $STATE_REAL/$ID.git-hooks. And fm_backlog_dispatch_rollback (bin/fm-backlog-transition-lib.sh:852) removes only the meta and busy records. So any fresh spawn that exits after line 4174 leaves the read-only directory behind. Examples: the effort-flag refusal at 4397, a meta prepare or publish failure at 4299/4304, and the kimi/rovo/agy readiness or delivery failures at 4557-4611.
Concrete sequence:
rm -rf has already deleted everything else in the home, including the .fm-secondmate-home marker. Teardown exits with that failure code, and a rerun fails with "REFUSED: ... is not a seeded secondmate home" (validate_firstmate_home_for_removal at :2411). The operator has to chmod and delete the remnants by hand. Before this round the leaked directory was writable and the same rm -rf succeeded, so this failure is new.
Fix, correcting the read-only change rather than extending it: in remove_firstmate_home, before safe_rm_rf (and before treehouse return, whose --force mode cleans the checkout), run
chmod u+w "$abs_home_path"/state/*.git-hooks 2>/dev/null || true. That shared removal point covers every way a directory can leak, including a killed spawn. Also remove $STATE_REAL/$ID.git-hooks (write bit first) in spawn_abort_cleanup when a fresh spawn's record is rolled back, so aborted spawns stop leaking these directories into any home.🔧 Fix applied.
1 warning still open:
bin/fm-spawn.sh:1227- The last fix round added a step to spawn_abort_cleanup: after a failed fresh spawn, if state/<id>.meta is gone, it deletes $STATE_REAL/$ID.git-hooks. The check is only whether the record exists. It never asks whether the agent that was already launched in that endpoint has been stopped. Deleting the directory under a running agent is worse than leaving it behind. The pane's GIT_CONFIG core.hooksPath still points at the deleted path. git silently skips a hooksPath that does not exist, so that agent's commits run no hooks at all: no strip, and none of the project's own hooks.Concrete path on the orca backend. At line 4396 ORCA_ABORT_CLEANUP is cleared once the record is published. Nothing on the later abort path closes the orca terminal: spawn_close_abort_endpoint returns early for orca, and SPAWN_LEASE_RETURN_ON_ABORT is only set for treehouse leases.
The same thing happens when a kimi gate fails but the agent is still running. kimi_spawn_fail never closes the endpoint, unlike rovo and agy, whose failure paths call rovo_endpoint_cleanup. An example is a delivery confirmation that timed out even though kimi did get the pointer, on orca or in a secondmate home, where there is no lease. Before this round, such an orphaned worker still had its strip.
Fix, narrowing what the ruling asked for rather than removing it: delete the directory only when no launched agent can still be running. Set a flag just before spawn_send_literal "$T" "$LAUNCH" at line 4558, and a second flag when spawn_close_abort_endpoint or rovo_endpoint_cleanup actually closes the endpoint. Remove the directory only if the launch was never sent or the endpoint was closed. The new kimi tests use a fake treehouse lease, where the endpoint is closed before the removal, so they keep passing. An orphan that is still running keeps its strip. If its directory later leaks into a secondmate home, remove_firstmate_home's new chmod already lets the home be deleted.
🔧 Fix applied.
✅ Re-checked - no issues remain.
bin/fm-spawn.sh:4189- When fm-spawn refuses a secondmate whose home is not a git checkout, it correctly exits 1 with no record and never sends a launch. But the tmux window it created earlier (fm-sm-nogit-probe, created at bin/fm-spawn.sh:3065) is left open as an empty shell. The cause predates this change. spawn_close_abort_endpoint only runs through spawn_return_abort_lease, which needs a treehouse lease, and a secondmate has none, so any late secondmate abort leaves its window behind. This change's fail-closed refusal is one more path into that. No agent runs there, so no trailer can land, and the strip guarantee holds. The operator just sees a stray window after a refused spawn. A narrow fix would close the endpoint on abort when SPAWN_LAUNCH_SENT=0, the case where it is always safe to close.git commit --trailer "Co-authored-by: Cursor <cursoragent@cursor.com>" -m ...;git cat-file commit HEADshows only `docs: add strip prob…Co-authored-by: Cursor <cursoragent@cursor.com>; the same --trailer command appears in live-cursor-before/cursor-pane.txt; no strip dir and…project commit-msg hook ran (chained): yes, and the message it saw contains no trailerCo-authored-by: Jane Doe <jane@example.com>, andmanaged-project pre-commit ran in .../projects/managed(real fm-spawn --secondm…error: could not install the AI-trailer strip hooks for sm-nogit-probe; no state record and no launch sent (an empty tmux window is left, se…fm-teardown.sh strip-probe-after --forceexits 0 and the dir is gonefm-teardown.sh sm-strip-probe --forceexits 0; the secondmate home and the parent-side strip dir are both goneCo-authored-by: Codex <noreply@openai.com>trailerbash tests/fm-git-strip-ai-trailers.test.sh(13 behavior cases driving real git commit through the installed hooksPath)bash tests/fm-kimi-harness.test.sh(includes the case where a failed spawn keeps the strip while the endpoint stays open)bash tests/fm-spawn-dispatch-profile.test.sh(launch-prefix wiring for cursor/grok/claude/codex secondmate)drive-live-cursor-spawn.sh after <worktree>: realfm-spawn.sh strip-probe-after <project> --harness cursor --mode local-only --yolo on --backend tmuxon a private tmux server, live cursor-agent 2026.09.15-d2fe57e with attributeCommitsToAgent=true, thengit cat-file commit HEADin the leased worktreedrive-live-cursor-spawn.sh before <git archive of e5316501>: the same live Cursor flow against the base commit, to reproduce the bugps ewwon the live cursor-agent process to confirm GIT_CONFIG_COUNT/KEY_0/VALUE_0 point core.hooksPath at state/<id>.git-hooksfm-teardown.sh strip-probe-after --forceon the live task: exit 0 and read-only strip dir removeddrive-secondmate-pane.sh: realfm-spawn.sh sm-strip-probe <git home> 'bash --noprofile --norc' --secondmate --backend tmux, then agit commit --trailerfor Cursor and a human co-author, run inside a managed project clone with its own pre-commit hookHook-manager attempts inside the live secondmate pane: overwrite commit-msg, add post-update, rm pre-commit and mv commit-msg in the dirgit rev-parse --git-path hooksreports, then another commit with a Cursor trailerfm-spawn.sh sm-nogit-probe <non-git seeded home> ... --secondmate: expect refusal and no recordfm-teardown.sh sm-strip-probe --forcewith a leaked read-onlystate/child-leaked.git-hooksinside the secondmate homeHARNESS=codex drive-live-cursor-spawn.shfor before and after: both blocked at Codex's directory-trust prompt and were torn down without answering it✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.