Skip to content

fix(bin): strip AI co-author trailers from commits by every fleet runtime - #57

Merged
tiago-peixoto merged 11 commits into
mainfrom
fm/firstmate-ai-coauthor-non-claude-runtimes
Sep 18, 2026
Merged

tiago-peixoto merged 11 commits into
mainfrom
fm/firstmate-ai-coauthor-non-claude-runtimes

Conversation

@tiago-peixoto

@tiago-peixoto tiago-peixoto commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

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

  • Added bin/fm-git-strip-ai-trailers.sh, a git commit-msg hook that removes AI Co-Authored-By trailers 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, plus Generated with ... Claude and Generated-by: lines. Human co-authors are left alone, and so is the author identity.
    Its install mode writes a read-only hooks directory. Besides commit-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.sh installs this directory at state/<id>.git-hooks for every launch kind and every runtime, Claude included. It points git at the directory by adding GIT_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.sh removes the directory for tasks and secondmate children, after first making it writable again.
  • Docs (configuration.md, scripts.md, AGENTS.md, Cursor harness reference) record the contract. They also list the gaps the owner accepted: git commit --no-verify skips the hook, a Made with <product> line is not matched, and hook managers cannot install from inside a fleet pane. A new tests/fm-git-strip-ai-trailers.test.sh covers the hook. Spawn and harness tests now assert the launch command's new prefix, and a shared fm_test_remove_tree in tests/lib.sh cleans up fixtures that contain the read-only directory.

🤖 Generated with Claude Code

Risk Assessment

⚠️ Medium: The latest fixer rounds (the launch-sent and endpoint-closed flags, whole-home chmod, and removal in the child-teardown loop) behave as ruled when traced, and I found no reachable defect, but the change routes every git hook in every fleet pane through read-only per-task wrappers, so its reach is wide enough to merit medium rather than low.

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-harness and fm-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 through fm-spawn with 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.

  • Live validation: ✅ go - 8 of 10 scenarios driven live against the product
Scenario Result Live Evidence
A Cursor crewmate spawned by fm-spawn with commit attribution ON commits its work, and the commit object has no Co-authored-by: Cursor trailer and keeps the operator's author identity ✅ pass live live-cursor-after/transcript.txt and SUMMARY.md: Cursor ran git commit --trailer &#34;Co-authored-by: Cursor &lt;cursoragent@cursor.com&gt;&#34; -m ...; git cat-file commit HEAD shows only `docs: add strip prob…
Bug reproduction: the identical live Cursor flow on the base commit lands the Cursor trailer on the commit object (the PR 55 shape) ✅ pass live live-cursor-before/transcript.txt: commit object ends with Co-authored-by: Cursor &lt;cursoragent@cursor.com&gt;; the same --trailer command appears in live-cursor-before/cursor-pane.txt; no strip dir and…
A project's own commit-msg hook still runs inside a fleet pane and sees the already-stripped message ✅ pass live live-cursor-after/transcript.txt: project commit-msg hook ran (chained): yes, and the message it saw contains no trailer
A secondmate's pane committing in a separate project clone it manages strips the Cursor trailer, keeps a human co-author, and runs that clone's own pre-commit hook, not the home's ✅ pass live live-secondmate-pane/transcript.txt: the commit object carries only Co-authored-by: Jane Doe &lt;jane@example.com&gt;, and managed-project pre-commit ran in .../projects/managed (real fm-spawn --secondm…
Adversarial: a hook manager inside the pane tries to overwrite, rename, add, or delete hooks in the dir git reports as the hooks dir; each write is refused and the next commit is still stripped ✅ pass live live-secondmate-pane/hook-manager-attempt.txt and transcript.txt: every attempt returned Permission denied with exit 1; the next commit with a Cursor --trailer produced a message with no trailer
Adversarial: spawning a secondmate whose home is not a git checkout fails closed instead of launching a runtime that cannot strip ✅ pass live live-secondmate-pane/transcript.txt: fm-spawn exit 1 with 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…
Tearing down a crewmate removes its read-only strip directory and succeeds ✅ pass live live-cursor-after/teardown.txt: before teardown the dir is dr-x------; fm-teardown.sh strip-probe-after --force exits 0 and the dir is gone
Tearing down a secondmate whose home holds a leaked read-only child strip dir removes the whole home without getting stuck ✅ pass live live-secondmate-pane/teardown.txt: fm-teardown.sh sm-strip-probe --force exits 0; the secondmate home and the parent-side strip dir are both gone
A Codex crewmate spawned by fm-spawn commits, and the commit object has no Co-authored-by: Codex &lt;noreply@openai.com&gt; trailer ⏸️ untested no Answering Codex 0.154.0's trust prompt for a fresh scratch project writes a trust entry into the user-level ~/.codex/config.toml, which this phase may not modify. To drive it, pre-trust a scratch proj…
A failed spawn whose launched agent's endpoint stayed open keeps that agent's strip dir ⏸️ untested no Driving this live needs a real runtime whose readiness or delivery gate fails while the agent keeps running (for example a kimi delivery confirmation that times out after kimi received the pointer). T…
Evidence: Before/after live Cursor commit objects summary

Source: Before/after live Cursor commit objects summary

# Live evidence: AI co-author trailer strip (base e5316501 vs change c278923b)

Same live flow both times: real bin/fm-spawn.sh Cursor crewmate (cursor-agent 2026.09.15-d2fe57e, commit attribution ON via a temp CURSOR_CONFIG_DIR) on a private tmux server, treehouse-leased worktree, brief asks for a local commit.

## Command Cursor typed (identical in both runs)
`` `
$ git add README.md && git commit --trailer "Co-authored-by: Cursor <cursoragent@cursor.com>" -m "$(cat <<'EOF'
`` `

## BEFORE (base e5316501): commit object
`` `
tree f5657403ed339d4bc63c4a12ff026c1ae9e68a88
parent 2df0a39a5058aac2090552f04cae5f513cb2ec7f
author Tiago Peixoto <tiagop@hey.com> 1789696614 -0300
committer Tiago Peixoto <tiagop@hey.com> 1789696614 -0300

docs: add strip probe line

Co-authored-by: Cursor <cursoragent@cursor.com>
`` `

## AFTER (change c278923b): commit object
`` `
tree f5657403ed339d4bc63c4a12ff026c1ae9e68a88
parent c34c197d40534c8187f01cf86b9a5836ccbb688d
author Tiago Peixoto <tiagop@hey.com> 1789696602 -0300
committer Tiago Peixoto <tiagop@hey.com> 1789696602 -0300

docs: add strip probe line
`` `

The project's own commit-msg hook still ran in the AFTER run and saw the stripped message (see live-cursor-after/transcript.txt).
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)

[22:56:07] tree under test: ~/.no-mistakes/worktrees/bbb16e1f0808/01M2RVXA817J3Y8YT3X2YK18YZ (c278923b)
[22:56:08] cursor-agent 2026.09.15-d2fe57e with attributeCommitsToAgent=true
[22:56:16] fm-spawn exit: 0
  spawn| warning: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/home/data/strip-probe-after/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode local-only - confirm its definition of done matches
  spawn| notice: strip-probe-after ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
  spawn| 🌳 Setting up worktree...
  spawn| 🌳 Leased worktree at /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project. Run 'treehouse return /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project' to release it.
  spawn| spawned strip-probe-after harness=cursor kind=ship mode=local-only yolo=on window=fleet:fm-strip-probe-after worktree=/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project
[22:56:16] task worktree: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project
[22:56:16] pane launch env (from ps):
  ps| 32746 ~/.local/bin/cursor-agent --use-system-ca ~/.local/share/cursor-agent/versions/2026.09.15-d2fe57e/index.js --trust --yolo --workspace /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project \040-cFIRSTMA
[22:56:16] strip dir: dr-x------ 15 hooks
0: ctl* (1 panes) [220x50] [layout ad1d,220x50,0,0,0] @0 (active)
1: fm-strip-probe-after (1 panes) [220x50] [layout ad1e,220x50,0,0,1] @1
[22:56:50] commit object (git cat-file commit HEAD) in the task worktree:
  obj| tree f5657403ed339d4bc63c4a12ff026c1ae9e68a88
  obj| parent c34c197d40534c8187f01cf86b9a5836ccbb688d
  obj| author Tiago Peixoto <tiagop@hey.com> 1789696602 -0300
  obj| committer Tiago Peixoto <tiagop@hey.com> 1789696602 -0300
  obj| 
  obj| docs: add strip probe line
[22:56:50] project commit-msg hook ran (chained): yes
  saw| docs: add strip probe line
  saw| 
[22:56:50] RESULT: no Co-authored-by trailer on the commit object
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)

cd -- '/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project'
tiago@Tiagos-MBP project % cd -- '/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project'
tiago@Tiagos-MBP project % export GOTMPDIR=/tmp/fm-strip-probe-after/gotmp
export FM_TASK_ID=strip-probe-after                                                                                                                                                                                         
tiago@Tiagos-MBP project % export FM_TASK_ID=strip-probe-after
tiago@Tiagos-MBP project % GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0='/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/home/state/strip-probe-after.git-hooks' env
 -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT -u FM_PI_HARNESS -u GEMINI_CLI -u CURSOR_INVOKED_AS '~/.local/bin/cursor-agent' --trust --yolo --workspace '/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000g
n/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project' "$('~/.no-mistakes/worktrees/bbb16e1f0808/01M2RVXA817J3Y8YT3X2YK18YZ/bin/fm-operational-input.sh' encode launch-brief < '/private/var/fo
lders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/home/data/strip-probe-after/launch-brief.md')"


  Cursor Agent
  v2026.09.15-d2fe57e

                                                                                                                                                                                                                           
  ⁣FIRSTMATE_OP: v1 launch-brief: # Current worker role contract                                                                                                                                                           
  You are a crewmate: an autonomous worker agent managed by firstmate.                                                                                                                                                     
  This section establishes your current identity before every project or task instruction below and supersedes any conflicting role identity in those instructions.                                                        
  Do the assigned work yourself and report only to firstmate; do not adopt a firstmate or secondmate supervisor identity, delegate the task, run fleet supervision, or address the captain.                                
  Your steering inbox is `/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/home/state/strip-probe-after.inbox`; this exact path belongs to your current task even when it is outside         
  the worktree or under the supervising firstmate home, so read and acknowledge its messages and do not reject it as another home's state.                                                                                 
  Never inspect or change any other home's endpoint namespace; this authorization is limited to the exact task paths named by this brief.                                                                                  
  When this task works on Firstmate itself, the repository root `AGENTS.md` (also imported by `CLAUDE.md`) is project content and the supervisor contract for the firstmate managing you: follow this brief instead        
  of that supervisor contract.                                                                                                                                                                                             
  Project instructions still govern the work wherever they do not conflict with this worker identity, including `CONTRIBUTING.md` and `firstmate-coding-guidelines` for Firstmate changes.                                 
                                                                                                                                                                                                                           
  # Task                                                                                                                                                                                                                   
  ## Captain's intent                                                                                                                                                                                                      
  Add one line to README.md and commit it locally.                                                                                                                                                                         
                                                                                                                                                                                                                           
  ## Firstmate spec                                                                                                                                                                                                        
  1. Append the exact line `strip probe` to README.md in your task worktree.                                                                                                                                               
  2. Commit that change with git, using the commit message "docs: add strip probe line".                                                                                                                                   
  3. Do not push and do not open a pull request. Stop once the commit exists.                                                                                                                                              
                                                                                                                                                                                                                           

  I'll append the exact line to README.md, commit it locally with the specified message, and check the steering inbox first.

    Read 2 files
    Read .
    Read README.md

  $ git status && git diff && git log -5 --oneline 2.3s
    … 1 output lines hidden · ctrl+o to expand
    nothing to commit, working tree clean
    c34c197 initial

  Inbox path wasn't found at first look. I'll check that location, then append the line and commit.

    Globbed "*" in ...05my380000gn/T/fm-strip-after.pgFvzU/home/state
    Found 18 files

    Edited README.md +1

    ▎  # strip probe project
    ▎+ strip probe                                                                                                                                                                                                      

  $ git status && git diff && git log -5 --oneline 569ms
    … 13 output lines hidden · ctrl+o to expand
    +strip probe
    c34c197 initial

  $ git add README.md && git commit --trailer "Co-authored-by: Cursor <cursoragent@cursor.com>" -m "$(cat <<'EOF'
    docs: add strip probe line 1.8s
    … 3 input + 2 output lines hidden · ctrl+o to expand
    Not currently on any branch.
    nothing to commit, working tree clean

  Steering inbox path acknowledged; strip-probe-after.inbox was not present in that state directory.

  Appended strip probe to README.md and created local commit f86326b with message docs: add strip probe line. Working tree is clean. Did not push and did not open a pull request.



                                                                                                                                                                                                                           
  → Add a follow-up                                                                                                                                                                                                        
                                                                                                                                                                                                                           

  Cursor Grok 4.6 High · 8.3% · 1 file edited                                                                                                                                                               Run Everything
  /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project · f86326b
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)

[22:56:09] tree under test: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-basetree.2P412S (archive)
[22:56:09] cursor-agent 2026.09.15-d2fe57e with attributeCommitsToAgent=true
[22:56:20] fm-spawn exit: 0
  spawn| warning: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/home/data/strip-probe-before/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode local-only - confirm its definition of done matches
  spawn| notice: strip-probe-before ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
  spawn| 🌳 Setting up worktree...
  spawn| 🌳 Leased worktree at /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project. Run 'treehouse return /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project' to release it.
  spawn| spawned strip-probe-before harness=cursor kind=ship mode=local-only yolo=on window=fleet:fm-strip-probe-before worktree=/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project
[22:56:20] task worktree: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project
[22:56:20] pane launch env (from ps):
  ps| 37834 ~/.local/bin/cursor-agent --use-system-ca ~/.local/share/cursor-agent/versions/2026.09.15-d2fe57e/index.js --trust --yolo --workspace /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project \040-cFIRSTM
[22:56:20] strip dir: absent
0: ctl* (1 panes) [220x50] [layout ad1d,220x50,0,0,0] @0 (active)
1: fm-strip-probe-before (1 panes) [220x50] [layout ad1e,220x50,0,0,1] @1
[22:57:02] commit object (git cat-file commit HEAD) in the task worktree:
  obj| tree f5657403ed339d4bc63c4a12ff026c1ae9e68a88
  obj| parent 2df0a39a5058aac2090552f04cae5f513cb2ec7f
  obj| author Tiago Peixoto <tiagop@hey.com> 1789696614 -0300
  obj| committer Tiago Peixoto <tiagop@hey.com> 1789696614 -0300
  obj| 
  obj| docs: add strip probe line
  obj| 
  obj| Co-authored-by: Cursor <cursoragent@cursor.com>
[22:57:02] project commit-msg hook ran (chained): yes
  saw| docs: add strip probe line
  saw| 
  saw| Co-authored-by: Cursor <cursoragent@cursor.com>
[22:57:02] RESULT: AI co-author trailer IS on the commit object
Evidence: Live Cursor pane capture on base commit

Source: Live Cursor pane capture on base commit

cd -- '/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project'
tiago@Tiagos-MBP project % cd -- '/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project'
tiago@Tiagos-MBP project % export GOTMPDIR=/tmp/fm-strip-probe-before/gotmp
export FM_TASK_ID=strip-probe-before                                                                                                                                                                                        
tiago@Tiagos-MBP project % export FM_TASK_ID=strip-probe-before
tiago@Tiagos-MBP project % env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT -u FM_PI_HARNESS -u GEMINI_CLI -u CURSOR_INVOKED_AS '~/.local/bin/cursor-agent' --trust --yolo --workspace '/private/var/folders/r8
/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project' "$('/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-basetree.2P412S/bin/fm-operational-input.sh' e
ncode launch-brief < '/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/home/data/strip-probe-before/launch-brief.md')"


  Cursor Agent
  v2026.09.15-d2fe57e

                                                                                                                                                                                                                           
  ⁣FIRSTMATE_OP: v1 launch-brief: # Current worker role contract                                                                                                                                                           
  You are a crewmate: an autonomous worker agent managed by firstmate.                                                                                                                                                     
  This section establishes your current identity before every project or task instruction below and supersedes any conflicting role identity in those instructions.                                                        
  Do the assigned work yourself and report only to firstmate; do not adopt a firstmate or secondmate supervisor identity, delegate the task, run fleet supervision, or address the captain.                                
  Your steering inbox is `/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/home/state/strip-probe-before.inbox`; this exact path belongs to your current task even when it is outside       
  the worktree or under the supervising firstmate home, so read and acknowledge its messages and do not reject it as another home's state.                                                                                 
  Never inspect or change any other home's endpoint namespace; this authorization is limited to the exact task paths named by this brief.                                                                                  
  When this task works on Firstmate itself, the repository root `AGENTS.md` (also imported by `CLAUDE.md`) is project content and the supervisor contract for the firstmate managing you: follow this brief instead        
  of that supervisor contract.                                                                                                                                                                                             
  Project instructions still govern the work wherever they do not conflict with this worker identity, including `CONTRIBUTING.md` and `firstmate-coding-guidelines` for Firstmate changes.                                 
                                                                                                                                                                                                                           
  # Task                                                                                                                                                                                                                   
  ## Captain's intent                                                                                                                                                                                                      
  Add one line to README.md and commit it locally.                                                                                                                                                                         
                                                                                                                                                                                                                           
  ## Firstmate spec                                                                                                                                                                                                        
  1. Append the exact line `strip probe` to README.md in your task worktree.                                                                                                                                               
  2. Commit that change with git, using the commit message "docs: add strip probe line".                                                                                                                                   
  3. Do not push and do not open a pull request. Stop once the commit exists.                                                                                                                                              
                                                                                                                                                                                                                           

  I'll append strip probe to README.md, commit it locally with the requested message, and check the steering inbox first.

    Read, globbed 2 files, 1 glob
    Read .
    Read README.md
    Globbed "README.md" in .

  Inbox path wasn’t a readable file; I’ll check that location, then append the line and commit.

  $ ls -la "/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/home/state/" 2>&1; echo "---"; ls -la
    "/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/home/state/strip-probe-before.inbox" 2>&1 exit 1 • 2.8s
    … 7 output lines hidden · ctrl+o to expand
    ---
    ls: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/home/state/strip-probe-before.inbox: No such file or directory

  $ git status && git diff && git log -5 --oneline 2.6s
    … 1 output lines hidden · ctrl+o to expand
    nothing to commit, working tree clean
    2df0a39 initial

  Steering inbox isn’t present yet; I’ll append the line and commit locally.

    Edited README.md +1

    ▎  # strip probe project
    ▎+ strip probe                                                                                                                                                                                                      

  $ git status && git diff && git log -5 --format='%s' 582ms
    … 13 output lines hidden · ctrl+o to expand
    +strip probe
    initial

  $ git add README.md && git commit --trailer "Co-authored-by: Cursor <cursoragent@cursor.com>" -m "$(cat <<'EOF'
    docs: add strip probe line 896ms
    … 3 input + 3 output lines hidden · ctrl+o to expand
    nothing to commit, working tree clean
    94d5a3c docs: add strip probe line

  Appended strip probe to README.md and committed it locally as 94d5a3c with message docs: add strip probe line. Did not push and did not open a pull request.

  Steering inbox path was acknowledged; the file was not present under home/state.



                                                                                                                                                                                                                           
  → Add a follow-up                                                                                                                                                                                                        
                                                                                                                                                                                                                           

  Cursor Grok 4.6 High · 8.4% · 1 file edited                                                                                                                                                               Run Everything
  /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-before.vW5IAs/treehouse/.treehouse/project-b7b0d5/1/project · 94d5a3c
Evidence: Crewmate teardown removes the read-only strip dir

Source: Crewmate teardown removes the read-only strip dir

# before teardown
dr-x------@ 17 tiago  staff  544 Sep 17 22:56 /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/home/state/strip-probe-after.git-hooks
# fm-teardown.sh strip-probe-after --force exit: 0
teardown: reaping leaked worktree process(es) for strip-probe-after: 23205 32746 34336 35207 37303
teardown: force-killing leaked worktree process(es) for strip-probe-after: 23205
🌳 Worktree returned to pool.
teardown strip-probe-after complete (window fleet:fm-strip-probe-after, worktree /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/treehouse/.treehouse/project-10670f/1/project)
Backlog: strip-probe-after just finished (this home keeps no markdown backlog at /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/home/data/backlog.md). Update /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/home/data/backlog.md - move strip-probe-after to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
# after teardown
ls: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-after.pgFvzU/home/state/strip-probe-after.git-hooks: No such file or directory
home-summary.json
terminal-outcomes
# tmux windows
ctl
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

== A/B: secondmate spawn with a git home (fm-spawn --secondmate, launch command: bash)
fm-spawn exit: 0
  spawn| warning: secondmate sm-strip-probe sync skipped before launch: primary default-branch commit cannot be resolved
  spawn| spawned sm-strip-probe harness=bash kind=secondmate mode=secondmate yolo=off window=fleet:fm-sm-strip-probe worktree=/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/sm-git
-- secondmate pane transcript:
  pane| export GOTMPDIR=/tmp/fm-sm-strip-probe/gotmp
  pane| tiago@Tiagos-MBP sm-git % export GOTMPDIR=/tmp/fm-sm-strip-probe/gotmp
  pane| tiago@Tiagos-MBP sm-git % GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0='/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe
  pane| .git-hooks' FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_PUBLIC_FOLLOWUP_PRIMARY_HOME='/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/f
  pane| m-strip-sm.JmvviX/parent' FM_HOME='/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/sm-git' FM_TRACE_CONTEXT=off FM_SUPERVISION_MODEL=persistent bash --noprofile --norc
  pane| bash-5.3$ clear
  pane| bash-5.3$ env | grep ^GIT_CONFIG_ | sort
  pane| GIT_CONFIG_COUNT=1
  pane| GIT_CONFIG_KEY_0=core.hooksPath
  pane| GIT_CONFIG_VALUE_0=/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks
  pane| bash-5.3$ cd '/private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/sm-git/projects/managed'
  pane| bash-5.3$ echo change >> README.md && git add README.md
  pane| bash-5.3$ git commit -q --trailer 'Co-authored-by: Cursor <cursoragent@cursor.com>' --trailer 'Co-authored-by: Jane Doe <jane@example.com>' -m 'fix: secondmate commit in a managed clone'
  pane| git cat-file commit HEAD
  pane| bash-5.3$ git cat-file commit HEAD
  pane| tree 41e1e8e2028d20837755626ed24f9b4efd809d92
  pane| parent 6a8544e5e6a7b03a36af269bc7552e66be01d9b9
  pane| author Tiago Peixoto <tiagop@hey.com> 1789696719 -0300
  pane| committer Tiago Peixoto <tiagop@hey.com> 1789696719 -0300
  pane| fix: secondmate commit in a managed clone
  pane| Co-authored-by: Jane Doe <jane@example.com>
  pane| bash-5.3$ cat .git/managed-pre-commit.ran
  pane| managed-project pre-commit ran in /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/sm-git/projects/managed
  pane| bash-5.3$ H=$(git rev-parse --path-format=absolute --git-path hooks); echo "hooks dir git reports in this pane: $H"
  pane| hooks dir git reports in this pane: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks
  pane| bash-5.3$ printf "#!/bin/sh\nexit 0\n" > "$H/commit-msg"; echo "overwrite commit-msg exit=$?"
  pane| bash: !/bin/sh\nexit: event not found
  pane| bash-5.3$ mv "$H/commit-msg" "$H/commit-msg.old"; echo "rename commit-msg exit=$?"
  pane| mv: rename /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks/commit-msg to /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks/commit-msg.old: Permission denied
  pane| rename commit-msg exit=1
  pane| bash-5.3$ git config --get core.hooksPath; echo "(repo-local core.hooksPath above, if any)"
  pane| /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks
  pane| (repo-local core.hooksPath above, if any)
  pane| bash-5.3$ echo DONE-SM-PANE
  pane| DONE-SM-PANE
  pane| bash-5.3$ 
-- managed clone commit object (read from outside the pane):
  obj| tree 41e1e8e2028d20837755626ed24f9b4efd809d92
  obj| parent 6a8544e5e6a7b03a36af269bc7552e66be01d9b9
  obj| author Tiago Peixoto <tiagop@hey.com> 1789696719 -0300
  obj| committer Tiago Peixoto <tiagop@hey.com> 1789696719 -0300
  obj| 
  obj| fix: secondmate commit in a managed clone
  obj| 
  obj| Co-authored-by: Jane Doe <jane@example.com>
-- strip dir after the hook-manager attempt: dr-x------@ commit-msg head: '~/.no-mistakes/worktrees/bbb16e1f0808/01M2RVXA817J3Y8YT3X2YK18YZ/bin

== C: secondmate spawn with a non-git home must fail closed
fm-spawn exit: 1
  spawn| warning: secondmate sm-nogit-probe sync skipped before launch: primary default-branch commit cannot be resolved
  spawn| fatal: not a git repository (or any of the parent directories): .git
  spawn| error: not a git worktree: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/sm-nogit
  spawn| error: could not install the AI-trailer strip hooks for sm-nogit-probe
tmux windows: ctl fm-sm-strip-probe fm-sm-nogit-probe 
parent state entries for sm-nogit-probe: 
Evidence: Secondmate pane: hook-manager writes refused, next commit still stripped

Source: Secondmate pane: hook-manager writes refused, next commit still stripped

bash-5.3$ echo "exit 0" > "$H/commit-msg"; echo "overwrite commit-msg exit=$?"
bash: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks/commit-msg: Permission denied
overwrite commit-msg exit=1
bash-5.3$ echo "exit 0" > "$H/post-update"; echo "add new hook exit=$?"
bash: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks/post-update: Permission denied
add new hook exit=1
bash-5.3$ rm -f "$H/pre-commit"; echo "delete pre-commit exit=$?"
rm: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks/pre-commit: Permission denied
delete pre-commit exit=1
bash-5.3$ echo again >> README.md && git commit -qam "fix: after hook-manager attempts" --trailer "Co-authored-by: Cursor <cursoragent@cursor.com>"; echo "commit exit=$?"
commit exit=0
bash-5.3$ git log -1 --format=%B
fix: after hook-manager attempts
bash-5.3$ echo DONE-B2
DONE-B2
bash-5.3$ 
Evidence: Secondmate teardown with a leaked read-only child strip dir

Source: Secondmate teardown with a leaked read-only child strip dir

# planted leaked child strip dir (as an aborted child spawn leaves it):
dr-x------@ 17 tiago  staff  544 Sep 17 22:59 /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/sm-git/state/child-leaked.git-hooks
# parent strip dir:
dr-x------@ 17 tiago  staff  544 Sep 17 22:58 /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks
# fm-teardown.sh sm-strip-probe --force exit: 0
teardown sm-strip-probe complete (window fleet:fm-sm-strip-probe, worktree /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/sm-git)
# after:
ls: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/sm-git: No such file or directory
ls: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-sm.JmvviX/parent/state/sm-strip-probe.git-hooks: No such file or directory
home-summary.json
Evidence: Codex first-run trust prompt that blocked the live Codex scenario

Source: Codex first-run trust prompt that blocked the live Codex scenario

> You are in /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-codexafter.cucKTH/treehouse/.treehouse/project-df3528/1/project
  Note: You’re in a subdirectory of a Git project. Trusting will apply to the repository root: /private/var/folders/r8/cylyt7xd7t50y9x5wc05my380000gn/T/fm-strip-codexafter.cucKTH/project
  Do you trust the contents of this directory? Working with untrusted contents comes with higher risk of prompt injection. Trusting the directory allows project-local config, hooks, and exec policies to load.
› 1. Yes, continue
  2. No, quit
  Press enter to continue

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-msg entry (the usual commitlint setup) and no core.hooksPath config runs pnpm install in its fresh worktree. The strip's commit-msg becomes commit-msg.old. From then on, git commit runs 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's has_core_hookpaths_set runs git config core.hooksPath). It refuses with "Cowardly refusing to install hooks with core.hooksPath set", 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 -rf sites), 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 in git rebase and git cherry-pick A..B, and git am runs the applypatch hooks once per patch.

Measured on this machine (git 2.50.1), using this change's installer with no project hooks present:

  • A 60-commit rebase took 894ms without the override and 12602ms with it (about 14x). Counting hooks shows 60 prepare-commit-msg and 60 post-commit invocations.
  • A single git commit -am went 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 to git rev-parse --git-path hooks, and a manual $PWD join for relative results. A single git rev-parse --path-format=absolute --git-path hooks already 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 ~/myhooks expanded 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 = &#34;$ours&#34; guard because ours comes from pwd -P. The repo already relies on --path-format=absolute in 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, and name=$(basename &#34;$0&#34;) with name=${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.com matches any address at those domains. So a human co-author who works there (for example Co-authored-by: Jane Doe &lt;jane@anthropic.com&gt; on a contribution to a vendor repo) is deleted from the commit with no message. The AI identities actually seen are narrower. Claude uses noreply@anthropic.com, and Cursor's cursoragent@cursor.com is already caught by the separate cursoragent@* rule. I also checked the installed Codex 0.154.0, which emits Co-authored-by: Codex &lt;noreply@openai.com&gt;, 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 plain rm -rf -- &#34;$target&#34;).

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:

  1. A secondmate spawns a kimi crewmate, and kimi never shows its ready signal.
  2. The spawn rolls back the meta, leaving <sm-home>/state/<child>.git-hooks read-only.
  3. Later the secondmate is torn down. cleanup_firstmate_home_children only walks state/*.meta, so it skips that directory.
  4. remove_firstmate_home runs rm -rf on the home. I reproduced this step on this machine with the installer: rm prints "Permission denied" for every wrapper, then "Directory not empty" up the tree, and returns 1.

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 &#34;$abs_home_path&#34;/state/*.git-hooks 2&gt;/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.

  1. A Cursor ship task is spawned on orca, and Cursor receives its brief in the launch command at line 4558.
  2. The backlog transition fails twice, for example a tasks-axi timeout. spawn_fresh_commit_rollback removes the record (line 4672), and the spawn exits 1.
  3. spawn_abort_cleanup sees status 1 and no meta, so it removes the strip directory.
  4. The Cursor agent keeps working in the still-open orca terminal and commits. Its Co-authored-by trailer lands on the commit object, which is the PR 55 shape the intent says must never happen again.

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.

⚠️ **Test** - 1 info
  • ℹ️ 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.
  • Live validation: ✅ go - 8 of 10 scenarios driven live against the product
Scenario Result Live Evidence
A Cursor crewmate spawned by fm-spawn with commit attribution ON commits its work, and the commit object has no Co-authored-by: Cursor trailer and keeps the operator's author identity ✅ pass live live-cursor-after/transcript.txt and SUMMARY.md: Cursor ran git commit --trailer &#34;Co-authored-by: Cursor &lt;cursoragent@cursor.com&gt;&#34; -m ...; git cat-file commit HEAD shows only `docs: add strip prob…
Bug reproduction: the identical live Cursor flow on the base commit lands the Cursor trailer on the commit object (the PR 55 shape) ✅ pass live live-cursor-before/transcript.txt: commit object ends with Co-authored-by: Cursor &lt;cursoragent@cursor.com&gt;; the same --trailer command appears in live-cursor-before/cursor-pane.txt; no strip dir and…
A project's own commit-msg hook still runs inside a fleet pane and sees the already-stripped message ✅ pass live live-cursor-after/transcript.txt: project commit-msg hook ran (chained): yes, and the message it saw contains no trailer
A secondmate's pane committing in a separate project clone it manages strips the Cursor trailer, keeps a human co-author, and runs that clone's own pre-commit hook, not the home's ✅ pass live live-secondmate-pane/transcript.txt: the commit object carries only Co-authored-by: Jane Doe &lt;jane@example.com&gt;, and managed-project pre-commit ran in .../projects/managed (real fm-spawn --secondm…
Adversarial: a hook manager inside the pane tries to overwrite, rename, add, or delete hooks in the dir git reports as the hooks dir; each write is refused and the next commit is still stripped ✅ pass live live-secondmate-pane/hook-manager-attempt.txt and transcript.txt: every attempt returned Permission denied with exit 1; the next commit with a Cursor --trailer produced a message with no trailer
Adversarial: spawning a secondmate whose home is not a git checkout fails closed instead of launching a runtime that cannot strip ✅ pass live live-secondmate-pane/transcript.txt: fm-spawn exit 1 with 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…
Tearing down a crewmate removes its read-only strip directory and succeeds ✅ pass live live-cursor-after/teardown.txt: before teardown the dir is dr-x------; fm-teardown.sh strip-probe-after --force exits 0 and the dir is gone
Tearing down a secondmate whose home holds a leaked read-only child strip dir removes the whole home without getting stuck ✅ pass live live-secondmate-pane/teardown.txt: fm-teardown.sh sm-strip-probe --force exits 0; the secondmate home and the parent-side strip dir are both gone
A Codex crewmate spawned by fm-spawn commits, and the commit object has no Co-authored-by: Codex &lt;noreply@openai.com&gt; trailer ⏸️ untested no Answering Codex 0.154.0's trust prompt for a fresh scratch project writes a trust entry into the user-level ~/.codex/config.toml, which this phase may not modify. To drive it, pre-trust a scratch proj…
A failed spawn whose launched agent's endpoint stayed open keeps that agent's strip dir ⏸️ untested no Driving this live needs a real runtime whose readiness or delivery gate fails while the agent keeps running (for example a kimi delivery confirmation that times out after kimi received the pointer). T…
  • bash 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 &lt;worktree&gt;: real fm-spawn.sh strip-probe-after &lt;project&gt; --harness cursor --mode local-only --yolo on --backend tmux on a private tmux server, live cursor-agent 2026.09.15-d2fe57e with attributeCommitsToAgent=true, then git cat-file commit HEAD in the leased worktree
  • drive-live-cursor-spawn.sh before &lt;git archive of e5316501&gt;: the same live Cursor flow against the base commit, to reproduce the bug
  • ps eww on the live cursor-agent process to confirm GIT_CONFIG_COUNT/KEY_0/VALUE_0 point core.hooksPath at state/<id>.git-hooks
  • fm-teardown.sh strip-probe-after --force on the live task: exit 0 and read-only strip dir removed
  • drive-secondmate-pane.sh: real fm-spawn.sh sm-strip-probe &lt;git home&gt; &#39;bash --noprofile --norc&#39; --secondmate --backend tmux, then a git commit --trailer for Cursor and a human co-author, run inside a managed project clone with its own pre-commit hook
  • Hook-manager attempts inside the live secondmate pane: overwrite commit-msg, add post-update, rm pre-commit and mv commit-msg in the dir git rev-parse --git-path hooks reports, then another commit with a Cursor trailer
  • fm-spawn.sh sm-nogit-probe &lt;non-git seeded home&gt; ... --secondmate: expect refusal and no record
  • fm-teardown.sh sm-strip-probe --force with a leaked read-only state/child-leaked.git-hooks inside the secondmate home
  • HARNESS=codex drive-live-cursor-spawn.sh for 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.

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
tiago-peixoto marked this pull request as ready for review September 18, 2026 03:45
@tiago-peixoto
tiago-peixoto merged commit 07dab42 into main Sep 18, 2026
14 checks passed
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant