Skip to content

fix(bin): trust Claude worktrees cut from a sibling clone of the same repo - #5

Merged
MrGTV-love merged 6 commits into
mainfrom
fm/fm-claude-trust-sibling-clones
Sep 29, 2026
Merged

MrGTV-love merged 6 commits into
mainfrom
fm/fm-claude-trust-sibling-clones

Conversation

@MrGTV-love

@MrGTV-love MrGTV-love commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Intent

The captain chose on 2026-09-28 to land firstmate fixes on his own fork, https://github.com/MrGTV-love/firstmate, instead of upstream and to avoid bothering Kun. He said to merge the fork PRs when green.

Land the Claude trust-guard fix in the existing fork PR #5 from branch fm/fm-claude-trust-sibling-clones. The fix makes the Claude trust check accept a treehouse pool worktree cut from a sibling clone of the same repository while continuing to refuse unrelated repositories. The PR branch conflicted with fork main, and its graft-cache untracking portion had already landed on fork main through fork PR 1.

What Changed

  • bin/fm-claude-trust.sh now accepts a linked worktree whose git common dir belongs to a different clone than <project>. It accepts it only when that owning checkout is the primary checkout for the common dir and both repos have the exact same origin URL. Trust and the consent-gated external-imports flags are then written against that sibling checkout. It prints an info: line on stderr. If the sibling already declined external imports, the whole registration refuses. Worktrees of unrelated repos are still refused.
  • tests/fm-claude-trust.test.sh adds three cases. A sibling-clone worktree with the same origin is trusted, and both the worktree and the sibling checkout are recorded. A sibling clone with declined imports is refused and the refusal names the sibling path. A different repo at a same-basename projects/ path is refused.
  • Comments in bin/fm-spawn.sh and bin/fm-agy-trust.sh are updated, plus the docs in .agents/skills/harness-adapters/references/harness/claude.md, docs/orca-backend.md and docs/verification/runtime-backends.md. They now describe the sibling-clone scope. They also note that fm-agy-trust.sh still refuses sibling-clone slots.

Risk Assessment

✅ Low: The sibling-clone acceptance is narrowly gated (verified primary checkout owning the worktree's common dir plus exact non-empty origin URL match), fails closed otherwise, is covered by behavioral tests, and the prior out-of-scope graft ignore rules were correctly removed so the branch carries only the intended trust-guard change.

Testing

I ran the targeted tests/fm-claude-trust.test.sh file, and every case passed. Then I drove the real trust script by hand against real git sibling clones in 9 scenarios, with the base-commit script as a before control. The last step was an A/B launch of the real Claude Code CLI on a private lab tmux socket. With no trust recorded, the trust dialog blocked the pane. After fm-claude-trust.sh ran with home B as the project, Claude reached its composer in the same sibling pool slot. Every scenario passed. I removed the lab directory and the tmux server, and the worktree is clean. This is a CLI and TUI change, so text pane captures are the visual evidence.

  • Live validation: ✅ go - 9 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Home B spawns into a shared pool slot cut from home A's clone (same origin): trust registers on the slot and on home A's checkout, with an info line ✅ pass live fm-claude-trust-cli-transcript.txt S1 AFTER: exit=0, store has $LAB/pool/slot1 and $LAB/homeA/projects/app trusted
Regression reproduction: the base-commit script refuses that same sibling pool slot ✅ pass live fm-claude-trust-cli-transcript.txt S1 BEFORE: base 6c174d8 exit=1 'is not a worktree of project', no store written
Real Claude Code opens the sibling pool slot with no trust dialog after registration, while an unregistered control shows the dialog ✅ pass live claude-live-trust-dialog-ab.txt: control shows 'Accessing workspace ... Yes, I trust this folder'; trusted pane shows the Claude Code v2.1.284 composer
Own-clone worktree still trusts home B's own checkout (no regression) ✅ pass live fm-claude-trust-cli-transcript.txt S2: exit=0, own1 + homeB trusted
Adversarial: worktree of an unrelated repo with a different origin and the same 'app' basename is refused and nothing is written ✅ pass live fm-claude-trust-cli-transcript.txt S3: exit=1, no store written
Adversarial: both repos have no origin, so empty origins do not count as a match, and the call is refused ✅ pass live fm-claude-trust-cli-transcript.txt S4: exit=1, no store written
Adversarial: sibling checkout already declined external imports, so the whole registration refuses, names the sibling path, and adds no trust ✅ pass live fm-claude-trust-cli-transcript.txt S5: exit=1, error names $LAB/homeA/projects/app, store unchanged
Sibling checkout already approved external imports, so the consent is carried to the worktree entry ✅ pass live fm-claude-trust-cli-transcript.txt S6: slot1 entry carries hasClaudeMdExternalIncludesApproved=true
Adversarial: a primary checkout (own or sibling, same origin) or a subdirectory of the sibling slot is refused ✅ pass live fm-claude-trust-cli-transcript.txt S7, S8, S9: exit=1, no store written
Full firstmate lab-primary spawn (fm-spawn through a shared treehouse pool across two lab homes) ⏸️ untested no A two-home shared treehouse pool inside a lab primary needs more fixture than this gate provides. The spawn code path did not change (only a comment in fm-spawn.sh). The trust script plus a real Claud…
Evidence: fm-claude-trust.sh CLI transcript (before/after + adversarial scenarios, store contents)

Source: fm-claude-trust.sh CLI transcript (before/after + adversarial scenarios, store contents)

\### Setup: one upstream origin; home A and home B each clone it (like fm-home-seed.sh).
\### Shared pool slot cut from home A's clone:
$LAB/homeA/projects/app/.git
\### Own-clone worktree of home B:
\### Unrelated repo (different origin), same basename 'app', with a worktree:
\### Unrelated repo with NO origin at all, while home B also stripped of origin copy:

===== S1 BEFORE (base 6c174d8): sibling-clone pool slot, project=home B =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg0 base-trust.sh $LAB/pool/slot1 $LAB/homeB/projects/app
error: refusing to pre-register Claude trust: '$LAB/pool/slot1' is not a worktree of project '$LAB/homeB/projects/app'
exit=1
(no store written)

===== S1 AFTER (this branch): sibling-clone pool slot, project=home B =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg1 fm-claude-trust.sh $LAB/pool/slot1 $LAB/homeB/projects/app
info: worktree '$LAB/pool/slot1' belongs to sibling clone '$LAB/homeA/projects/app' (origin match)
trusted: $LAB/pool/slot1
trusted (project root): $LAB/homeA/projects/app
exit=0
{
 "$LAB/pool/slot1": {
  "hasTrustDialogAccepted": true
 },
 "$LAB/homeA/projects/app": {
  "hasTrustDialogAccepted": true
 }
}

===== S2 regression: own-clone worktree, project=home B =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg2 fm-claude-trust.sh $LAB/pool/own1 $LAB/homeB/projects/app
trusted: $LAB/pool/own1
trusted (project root): $LAB/homeB/projects/app
exit=0
{
 "$LAB/pool/own1": {
  "hasTrustDialogAccepted": true
 },
 "$LAB/homeB/projects/app": {
  "hasTrustDialogAccepted": true
 }
}

===== S3 adversarial: unrelated repo (different origin, same basename) =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg3 fm-claude-trust.sh $LAB/pool/foreign1 $LAB/homeB/projects/app
error: refusing to pre-register Claude trust: '$LAB/pool/foreign1' is not a worktree of project '$LAB/homeB/projects/app'
exit=1
(no store written)

===== S4 adversarial: both sides have NO origin (empty == empty must not match) =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg4 fm-claude-trust.sh $LAB/pool/noorigin1 $LAB/homeC/projects/app
error: refusing to pre-register Claude trust: '$LAB/pool/noorigin1' is not a worktree of project '$LAB/homeC/projects/app'
exit=1
(no store written)

===== S5 adversarial: sibling clone A already DECLINED external imports =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg5 fm-claude-trust.sh $LAB/pool/slot1 $LAB/homeB/projects/app
info: worktree '$LAB/pool/slot1' belongs to sibling clone '$LAB/homeA/projects/app' (origin match)
error: project entry for $LAB/homeA/projects/app in $LAB/cfg5/.claude.json already declined external CLAUDE.md imports; refusing to override that consent. To recover, remove hasClaudeMdExternalIncludesApproved and hasClaudeMdExternalIncludesWarningShown from that project entry and approve the imports dialog interactively once
error: refusing to pre-register Claude trust: could not record trust for '$LAB/pool/slot1' and project '$LAB/homeA/projects/app' in '$LAB/cfg5/.claude.json'
exit=1
{
 "$LAB/homeA/projects/app": {
  "hasTrustDialogAccepted": false,
  "hasClaudeMdExternalIncludesApproved": false,
  "hasClaudeMdExternalIncludesWarningShown": true
 }
}

===== S6: sibling clone A already APPROVED external imports -> consent carried to worktree =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg6 fm-claude-trust.sh $LAB/pool/slot1 $LAB/homeB/projects/app
info: worktree '$LAB/pool/slot1' belongs to sibling clone '$LAB/homeA/projects/app' (origin match)
trusted: $LAB/pool/slot1
trusted (project root): $LAB/homeA/projects/app
exit=0
{
 "$LAB/homeA/projects/app": {
  "hasTrustDialogAccepted": true,
  "hasClaudeMdExternalIncludesApproved": true,
  "hasClaudeMdExternalIncludesWarningShown": true
 },
 "$LAB/pool/slot1": {
  "hasTrustDialogAccepted": true,
  "hasClaudeMdExternalIncludesApproved": true,
  "hasClaudeMdExternalIncludesWarningShown": true
 }
}

===== S7 adversarial: home B's own primary checkout passed as the worktree =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg7 fm-claude-trust.sh $LAB/homeB/projects/app $LAB/homeB/projects/app
error: refusing to pre-register Claude trust: '$LAB/homeB/projects/app' is a primary checkout, not an isolated worktree
exit=1
(no store written)

===== S8 adversarial: home A's primary checkout (sibling, same origin) passed as the worktree =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg8 fm-claude-trust.sh $LAB/homeA/projects/app $LAB/homeB/projects/app
error: refusing to pre-register Claude trust: '$LAB/homeA/projects/app' is a primary checkout, not an isolated worktree
exit=1
(no store written)

===== S9 adversarial: subdirectory of the sibling pool slot =====
$ CLAUDE_CONFIG_DIR=$LAB/cfg9 fm-claude-trust.sh $LAB/pool/slot1/sub $LAB/homeB/projects/app
error: refusing to pre-register Claude trust: '$LAB/pool/slot1/sub' is not a worktree root (its root is '$LAB/pool/slot1')
exit=1
(no store written)
Evidence: Real Claude Code A/B pane capture: trust dialog without registration vs composer after registration in sibling pool slot

Source: Real Claude Code A/B pane capture: trust dialog without registration vs composer after registration in sibling pool slot

Real Claude Code 2.1.284 (Claude Code) launched in the SAME sibling-clone pool slot ($LAB/pool/slot1, cut from home A's clone), two isolated CLAUDE_CONFIG_DIRs.

=== CONTROL: no trust recorded -> trust dialog blocks the pane ===

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Accessing workspace:

 $LAB/pool/slot1

 Quick safety check: Is this a project you created or one you trust? (Like your own code, a well-known open source project, or work from your team). If not,
 take a moment to review what's in this folder first.

 Claude Code'll be able to read, edit, and execute files here.

 Security guide

 ❯ No, exit
   Yes, I trust this folder

 Enter to confirm · Esc to cancel

=== TRUSTED: after 'fm-claude-trust.sh $LAB/pool/slot1 $LAB/homeB/projects/app' -> no dialog, composer reached ===
 ▐▛███▛█   Claude Code v2.1.284
▝▜██████▀  Opus 5.5 · API Usage Billing
 ▝▝   ▝▝   $LAB/pool/slot1

▎ Auto mode is now Claude Code's default permission mode.
▎ Auto mode lets Claude handle permission prompts automatically. Claude checks each tool call for risky actions and prompt injection before executing, runs the
▎ ones it assesses as lower-risk, and blocks the rest.
▎ https://code.claude.com/docs/en/permission-modes

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on (shift+tab to cycle) · ← for agents                                                                               Not logged in · Run /login
                                                          tmux focus-events off · add 'set -g focus-events on' to ~/.tmux.conf and reattach for focus tracking

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 1 issue found → auto-fixed ✅
  • ⚠️ .agents/skills/harness-adapters/references/harness/claude.md - merge conflict rebasing onto origin/fm/fm-claude-trust-sibling-clones

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ .ignore:1 - Unrequired component: commit 192abdf (a 'docs' commit) adds a new /graft/ rule to .gitignore and a new top-level .ignore file that re-admits graft/ to ripgrep search. The user intent covers only the Claude trust-guard sibling-clone fix. It says the graft-cache untracking 'had already landed on fork main through fork PR 1' (6ff611a, which added only graft/.cache/). No intent requirement needs a whole-directory /graft/ ignore or a new ripgrep .ignore policy, and neither is part of what PR 1 landed. With /graft/ in place, the existing graft/.cache/ line from PR 1 also becomes redundant. Recommended remedy: drop the .gitignore hunk and the new .ignore file from this branch, so the PR carries only the trust-guard change and main's existing graft ignore stays as it is.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 9 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Home B spawns into a shared pool slot cut from home A's clone (same origin): trust registers on the slot and on home A's checkout, with an info line ✅ pass live fm-claude-trust-cli-transcript.txt S1 AFTER: exit=0, store has $LAB/pool/slot1 and $LAB/homeA/projects/app trusted
Regression reproduction: the base-commit script refuses that same sibling pool slot ✅ pass live fm-claude-trust-cli-transcript.txt S1 BEFORE: base 6c174d8 exit=1 'is not a worktree of project', no store written
Real Claude Code opens the sibling pool slot with no trust dialog after registration, while an unregistered control shows the dialog ✅ pass live claude-live-trust-dialog-ab.txt: control shows 'Accessing workspace ... Yes, I trust this folder'; trusted pane shows the Claude Code v2.1.284 composer
Own-clone worktree still trusts home B's own checkout (no regression) ✅ pass live fm-claude-trust-cli-transcript.txt S2: exit=0, own1 + homeB trusted
Adversarial: worktree of an unrelated repo with a different origin and the same 'app' basename is refused and nothing is written ✅ pass live fm-claude-trust-cli-transcript.txt S3: exit=1, no store written
Adversarial: both repos have no origin, so empty origins do not count as a match, and the call is refused ✅ pass live fm-claude-trust-cli-transcript.txt S4: exit=1, no store written
Adversarial: sibling checkout already declined external imports, so the whole registration refuses, names the sibling path, and adds no trust ✅ pass live fm-claude-trust-cli-transcript.txt S5: exit=1, error names $LAB/homeA/projects/app, store unchanged
Sibling checkout already approved external imports, so the consent is carried to the worktree entry ✅ pass live fm-claude-trust-cli-transcript.txt S6: slot1 entry carries hasClaudeMdExternalIncludesApproved=true
Adversarial: a primary checkout (own or sibling, same origin) or a subdirectory of the sibling slot is refused ✅ pass live fm-claude-trust-cli-transcript.txt S7, S8, S9: exit=1, no store written
Full firstmate lab-primary spawn (fm-spawn through a shared treehouse pool across two lab homes) ⏸️ untested no A two-home shared treehouse pool inside a lab primary needs more fixture than this gate provides. The spawn code path did not change (only a comment in fm-spawn.sh). The trust script plus a real Claud…
  • bash tests/fm-claude-trust.test.sh (targeted file: all cases ok, including the 3 new sibling-clone cases and the fake-claude fm-spawn cases)
  • Manual run of bin/fm-claude-trust.sh against real git fixtures (a bare origin, home A and home B clones, a shared pool slot from A, an own-clone worktree, an unrelated repo, repos with no origin) with isolated CLAUDE_CONFIG_DIR stores, 9 scenarios
  • Before/after control: git show 6c174d8:bin/fm-claude-trust.sh run on the same sibling pool slot. The base version refuses it.
  • Real claude 2.1.284 launched on a private tmux -L fm-lab socket (TMUX_TMPDIR inside the lab) in the sibling pool slot: a control store with no trust vs a store written by fm-claude-trust.sh. Captured with tmux capture-pane, then kill-server and rm -rf of the lab.
  • git diff --stat 6c174d8..5013e5f confirms no .gitignore/.ignore hunks (review-1 decision) and that the worker-account-pin wording is kept in claude.md (rebase-1 decision)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…ository

When multiple firstmate homes share one treehouse worktree pool, a pool slot
may belong to a sibling clone of the project rather than the spawning home's
own clone. The scope test now accepts these through two checks:

1. Origin URL comparison: normalize and compare git remote origins
2. Path-based heuristic: check if the worktree's primary checkout lives
   at <firstmate-home>/projects/<basename> and verify same repo via origin

When a sibling clone is detected, trust and external-imports consent are
registered against the sibling checkout (Claude Code's canonicalization
target), not the spawning home's clone.

New tests: sibling clone with same origin accepted, sibling clone with
declined imports refused, different repo at same basename path refused.
@MrGTV-love
MrGTV-love force-pushed the fm/fm-claude-trust-sibling-clones branch from 3a0efd5 to 99a6ed4 Compare September 29, 2026 01:18
@MrGTV-love MrGTV-love changed the title fm-claude-trust: accept worktrees from sibling clones of the same repository fix(bin): trust Claude worktrees cut from a sibling clone of the same repo Sep 29, 2026
@MrGTV-love
MrGTV-love merged commit 3e2e492 into main Sep 29, 2026
21 of 22 checks passed
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