fix: prevent worktree isolation bypass via prompt and git-level adoption#1198
fix: prevent worktree isolation bypass via prompt and git-level adoption#1198
Conversation
…ion (#1193, #1188) Three fixes for workflows operating on wrong branches: - archon-implement prompt: replace ambiguous branch table with decision tree that trusts the worktree isolation system, uses $BASE_BRANCH explicitly, and instructs AI to never switch branches - WorktreeProvider.findExisting: verify worktree's parent repo matches the request before adopting, preventing cross-clone adoption - WorktreeProvider.createNewBranch: reset stale orphan branches to the intended start-point instead of silently inheriting old commits Fixes #1193 Relates to #1188
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdded strict worktree ownership verification and refined branch-reuse logic in the isolation provider: existing worktrees are now verified by reading and resolving their Changes
Sequence Diagram(s)sequenceDiagram
participant Req as Request (Isolation)
participant Provider as WorktreeProvider
participant FS as Filesystem
participant Git as Git CLI
participant Log as Logger
Req->>Provider: findExisting(request)
Provider->>FS: readFile(worktreePath + "/.git")
alt read succeeds & contains "gitdir: <path>"
Provider->>Provider: resolve referenced repo root
Provider->>Req: compare resolvedRoot == request.canonicalRepoPath?
alt match
Provider->>Log: worktree_adopted
Provider->>Git: (no branch switch) use existing branch / proceed
else mismatch
Provider->>Log: worktree_adoption_refused_cross_checkout
Provider->>Req: throw ownership verification error
end
else read fails / invalid format
Provider->>Log: worktree_adoption_refused_invalid_dotgit
Provider->>Req: throw verification error
end
Note over Provider,Git: When creating/using branch
Provider->>Git: git branch -f <branch> <startPoint> (if branch exists)
Provider->>Git: git worktree add ...
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
packages/isolation/src/providers/worktree.ts (1)
527-540: Windows path separator may not match.The regex
/gitdir: (.+)\/\.git\/worktrees\//expects forward slashes. On Windows, the.gitfile content typically uses forward slashes even on Windows, but git's behavior can vary by version/configuration. If a Windows system writes backslashes, the regex won't match.Consider normalizing the path or using a more flexible regex:
- const match = /gitdir: (.+)\/\.git\/worktrees\//.exec(gitContent); + const match = /gitdir: (.+)[/\\]\.git[/\\]worktrees[/\\]/.exec(gitContent);Also, consider adding an explicit return type annotation for
getWorktreeSourceRepoper the strict TypeScript coding guideline.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/isolation/src/providers/worktree.ts` around lines 527 - 540, The getWorktreeSourceRepo function's regex (/gitdir: (.+)\/\.git\/worktrees\//) assumes forward slashes and can fail on Windows; update getWorktreeSourceRepo to be tolerant of either separator by normalizing gitContent path separators or using a flexible regex that accepts both forward and backslashes (e.g., match both '/' and '\\') when extracting the repo root from the "gitdir:" line, and keep the existing try/catch behavior returning null on failure; reference the getWorktreeSourceRepo function and the readFile call to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.archon/commands/defaults/archon-implement.md:
- Around line 111-127: The doc contains a contradiction: the directive "Never
switch branches or create new branches" conflicts with the decision-tree path
that runs "git checkout -b feature/{plan-slug}", and the fenced diagram is
missing a markdown language specifier; update the decision tree to match the
directive by removing or disabling the branch-creation path (the node that
invokes "git checkout -b feature/{plan-slug}" and the branch-creation branch),
or alternatively revise the directive to clearly allow branch creation only in
non-worktree contexts (explicitly mention the exception), and add a language tag
(e.g., change the opening fence to ```text) to the fenced diagram to satisfy
markdownlint MD040.
In `@packages/isolation/src/providers/worktree.test.ts`:
- Around line 37-43: Add clearing for the additional mocks to prevent test
pollution: in the afterEach block where mockAccess.mockClear() is called, also
call mockReadFile.mockClear() and mockRm.mockClear() so the mock state for
readFile (mockReadFile) and rm (mockRm) is reset between tests; locate the
afterEach in worktree.test.ts and add these two mockClear() calls beside
mockAccess.mockClear().
In `@packages/isolation/src/providers/worktree.ts`:
- Around line 487-503: Normalize both repository paths before comparing: when
you call getWorktreeSourceRepo(worktreePath) and compare its result
(existingRepo) to request.canonicalRepoPath, run both through path.resolve (or
path.normalize) to remove trailing slashes and normalize separators (and require
importing Node's path). Update the comparison to use normalizedExistingRepo and
normalizedRequestRepo so worktree_adoption_skipped_cross_checkout is triggered
reliably across platforms; keep the same logging fields (worktreePath,
branchName, existingRepo, requestRepo) but consider logging the normalized
values if helpful.
---
Nitpick comments:
In `@packages/isolation/src/providers/worktree.ts`:
- Around line 527-540: The getWorktreeSourceRepo function's regex (/gitdir:
(.+)\/\.git\/worktrees\//) assumes forward slashes and can fail on Windows;
update getWorktreeSourceRepo to be tolerant of either separator by normalizing
gitContent path separators or using a flexible regex that accepts both forward
and backslashes (e.g., match both '/' and '\\') when extracting the repo root
from the "gitdir:" line, and keep the existing try/catch behavior returning null
on failure; reference the getWorktreeSourceRepo function and the readFile call
to locate the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8fb26aaf-e91c-4821-af27-c04de20f2cd3
📒 Files selected for processing (3)
.archon/commands/defaults/archon-implement.mdpackages/isolation/src/providers/worktree.test.tspackages/isolation/src/providers/worktree.ts
PR Review SummaryRan 4 specialized reviewers: code-reviewer, docs-impact, pr-test-analyzer, silent-failure-hunter. Critical Issues (2)
Important Issues (2)
Suggestions
Strengths
Verdict: NEEDS FIXESThe stale-branch-reset logic and the Recommended Actions
|
…prompts Address CodeRabbit + self-review findings on #1198: Code fixes: - findExisting now throws on cross-checkout or unverifiable state instead of returning null, avoiding a confusing cascade through createNewBranch - verifyWorktreeOwnership handles .git errors precisely: ENOENT/EACCES/EIO throw a fail-fast error; EISDIR (full checkout at path) throws a clear "not a worktree" error; unmatched gitdir (submodule, malformed) throws - Path comparison uses resolve() to normalize trailing slashes - Added classifyIsolationError patterns so new errors produce actionable user messages Test fixes: - mockClear readFile/rm in afterEach - New tests: cross-checkout throws, EISDIR throws, EACCES throws, submodule pointer throws, trailing-slash normalization, branch -f reset failure propagates without retry - Updated existing tests that relied on permissive adoption to provide valid matching gitdir Prompt fixes (sweep of all default commands): - archon-implement.md: clarify "never switch branches" applies to worktree context; non-worktree branch creation still allowed - archon-fix-issue.md + archon-implement-issue.md: aligned decision tree with archon-implement pattern; use $BASE_BRANCH instead of MAIN/MASTER - archon-plan-setup.md: converted table to ordered decision tree with IN WORKTREE? first; removed ambiguous "already on correct feature branch" row
Review ResponseAddressed all critical + important findings from CodeRabbit and self-review. Critical fixes1. Cross-checkout detection now throws (worktree.ts:487-493) Previously 2. Narrowed catch in ownership check (worktree.ts:531-580) Replaced
No permissive fallback — the bug this PR fixes was exactly "default to adoption when unsure," so defaulting that way on error recreates the vulnerability. Important fixes3. Test 4. Aligned sibling command prompts Swept all 36 default commands. Found 3 more with the same ambiguous decision tree and fixed them:
All three now match Other 33 commands are clean: legitimate PR-branch checkouts (implement-review-fixes, auto-fix-review, etc.), user bootstrap commands, error-message documentation, or worktree-path artifacts. Also fixed (minor)
Validation
Deliberately not addressed
|
|
Credit: @halindrome's #1186 identified the root cause class and explicitly flagged this provider-layer gap as a known limitation, which directly shaped this PR's approach. Thanks for the groundwork! |
#1188) PR #1198 guarded WorktreeProvider.findExisting(), but IsolationResolver has three earlier adoption paths that bypass the provider layer: - findReusable (DB lookup by workflow identity) - findLinkedIssueEnv (cross-reference via linked issues) - tryBranchAdoption (PR branch discovery) Two clones of the same remote share codebase_id (identity is derived from owner/repo). Without these guards, clone B silently adopts clone A's worktree via any of the three paths. Changes: - Extract verifyWorktreeOwnership from WorktreeProvider (private) to @archon/git/src/worktree.ts as an exported function, sitting next to getCanonicalRepoPath which parses the same .git file format - Call the shared function from all three resolver paths; throw on cross-clone mismatch (DB rows are preserved — they legitimately belong to the other clone) - Compute canonicalRepoPath once at the top of resolve() - Six new tests in resolver.test.ts covering each guarded path's cross-checkout and same-clone behaviors Fixes #1183 Fixes #1188 (part 1 — cross-checkout; part 2 parallel collision deferred to follow-up alongside #1036)
* fix: extend worktree ownership guard to resolver adoption paths (#1183, #1188) PR #1198 guarded WorktreeProvider.findExisting(), but IsolationResolver has three earlier adoption paths that bypass the provider layer: - findReusable (DB lookup by workflow identity) - findLinkedIssueEnv (cross-reference via linked issues) - tryBranchAdoption (PR branch discovery) Two clones of the same remote share codebase_id (identity is derived from owner/repo). Without these guards, clone B silently adopts clone A's worktree via any of the three paths. Changes: - Extract verifyWorktreeOwnership from WorktreeProvider (private) to @archon/git/src/worktree.ts as an exported function, sitting next to getCanonicalRepoPath which parses the same .git file format - Call the shared function from all three resolver paths; throw on cross-clone mismatch (DB rows are preserved — they legitimately belong to the other clone) - Compute canonicalRepoPath once at the top of resolve() - Six new tests in resolver.test.ts covering each guarded path's cross-checkout and same-clone behaviors Fixes #1183 Fixes #1188 (part 1 — cross-checkout; part 2 parallel collision deferred to follow-up alongside #1036) * fix: address PR review — polish, observability, secondary gap, docs Addresses the multi-agent review on #1206: Code fixes: - worktree.adoption_refused_cross_checkout log event renamed to match CLAUDE.md {domain}.{action}_{state} convention - verifyWorktreeOwnership now preserves err.code and err via { cause } when wrapping fs errors, so classifyIsolationError is robust to Node message format changes - Structured fields (codebaseId, canonicalRepoPath) added to all cross-clone rejection logs for incident debugging - Wrap getCanonicalRepoPath at top of resolve() with classified error instead of letting it propagate as an unclassified crash - Extract assertWorktreeOwnership helper on IsolationResolver — centralizes warn-then-rethrow contract, removes duplication - Dedupe toWorktreePath(existing.working_path) calls in resolver paths - Add code comment on findLinkedIssueEnv explaining why throw-on-first is intentional (user decision — surfaces anomaly instead of masking) Secondary gap closed: - WorktreeProvider.findExisting PR-branch adoption path (findWorktreeByBranch) now also verifies ownership — same class of bug as the main path, just via a different lookup Tests: - 8 new unit tests for verifyWorktreeOwnership in @archon/git (matching pointer, different clone, EISDIR/ENOENT errno preservation, submodule pointer, corrupted .git, trailing-slash normalization, cause chain) - tryBranchAdoption cross-clone test now asserts store.create was never called (symmetry with paths 1+2 asserting updateStatus) - New test for cross-clone rejection in the PR-branch-adoption secondary path in worktree.test.ts Docs: - CHANGELOG.md Unreleased entry for the cross-clone fix series - troubleshooting.md "Worktree Belongs to a Different Clone" section documenting all four new error patterns with resolution steps and pointer to #1192 for the architectural fix * fix(git): use raw .git pointer in cross-clone error message verifyWorktreeOwnership previously called path.resolve() on the gitdir path before embedding it in the error message. On Windows, resolve() prepends a drive letter to a POSIX-style path (e.g., /other/clone → C:\other\clone), which: 1. Misled users by showing a path that doesn't match what's actually in their .git file 2. Broke a Windows-only test asserting the error contains the literal /other/clone path Compare on resolved paths (correct — normalizes trailing slashes and relative components for the equality check) but display the raw match in the error message (recognizable to the user).
Worktrees created via `git worktree add` do not initialize submodules — monorepo workflows that need submodule content find empty directories. Auto-detect `.gitmodules` and run `git submodule update --init --recursive` after worktree creation; classify failures through the isolation error pipeline. Behavior: - `.gitmodules` absent → skip silently (zero-cost probe, no effect on non-submodule repos) - `.gitmodules` present → run submodule init by default (opt out via `worktree.initSubmodules: false`) - submodule init or `.gitmodules` read failure → throw with classified error including opt-out guidance - Only `ENOENT` on `.gitmodules` is treated as "no submodules"; other access errors (EACCES, EIO) surface as failures to prevent silent empty-dir worktrees Changes: - `packages/isolation/src/providers/worktree.ts` — `initSubmodules()` method + call site in `createWorktree()` - `packages/isolation/src/errors.ts` — collapsed `errorPatterns` + `knownPatterns` into single `ERROR_PATTERNS` source of truth with `known: boolean` per entry; added submodule pattern with opt-out guidance - `packages/isolation/src/types.ts` + `packages/core/src/config/config-types.ts` — new `initSubmodules?: boolean` config option - `packages/docs-web/src/content/docs/reference/configuration.md` — documented the new option and submodule behavior - Tests: default-on, explicit opt-in, explicit opt-out, skip-when-absent, fail-fast on EACCES, fail-fast on git failure, fail-fast on timeout Credit to @halindrome for the original implementation and root-cause mapping across #1183, #1187, #1188, #1192. Follow-up: #1192 (codebase identity rearchitect) would retire the cross-clone guard code in `resolver.ts` and `worktree.ts` that #1198, #1206 added. Separate PR. Closes #1187
…ion (coleam00#1198) * fix: prevent worktree isolation bypass via prompt and git-level adoption (coleam00#1193, coleam00#1188) Three fixes for workflows operating on wrong branches: - archon-implement prompt: replace ambiguous branch table with decision tree that trusts the worktree isolation system, uses $BASE_BRANCH explicitly, and instructs AI to never switch branches - WorktreeProvider.findExisting: verify worktree's parent repo matches the request before adopting, preventing cross-clone adoption - WorktreeProvider.createNewBranch: reset stale orphan branches to the intended start-point instead of silently inheriting old commits Fixes coleam00#1193 Relates to coleam00#1188 * fix: address PR review — strict worktree verification, align sibling prompts Address CodeRabbit + self-review findings on coleam00#1198: Code fixes: - findExisting now throws on cross-checkout or unverifiable state instead of returning null, avoiding a confusing cascade through createNewBranch - verifyWorktreeOwnership handles .git errors precisely: ENOENT/EACCES/EIO throw a fail-fast error; EISDIR (full checkout at path) throws a clear "not a worktree" error; unmatched gitdir (submodule, malformed) throws - Path comparison uses resolve() to normalize trailing slashes - Added classifyIsolationError patterns so new errors produce actionable user messages Test fixes: - mockClear readFile/rm in afterEach - New tests: cross-checkout throws, EISDIR throws, EACCES throws, submodule pointer throws, trailing-slash normalization, branch -f reset failure propagates without retry - Updated existing tests that relied on permissive adoption to provide valid matching gitdir Prompt fixes (sweep of all default commands): - archon-implement.md: clarify "never switch branches" applies to worktree context; non-worktree branch creation still allowed - archon-fix-issue.md + archon-implement-issue.md: aligned decision tree with archon-implement pattern; use $BASE_BRANCH instead of MAIN/MASTER - archon-plan-setup.md: converted table to ordered decision tree with IN WORKTREE? first; removed ambiguous "already on correct feature branch" row
…am00#1206) * fix: extend worktree ownership guard to resolver adoption paths (coleam00#1183, coleam00#1188) PR coleam00#1198 guarded WorktreeProvider.findExisting(), but IsolationResolver has three earlier adoption paths that bypass the provider layer: - findReusable (DB lookup by workflow identity) - findLinkedIssueEnv (cross-reference via linked issues) - tryBranchAdoption (PR branch discovery) Two clones of the same remote share codebase_id (identity is derived from owner/repo). Without these guards, clone B silently adopts clone A's worktree via any of the three paths. Changes: - Extract verifyWorktreeOwnership from WorktreeProvider (private) to @archon/git/src/worktree.ts as an exported function, sitting next to getCanonicalRepoPath which parses the same .git file format - Call the shared function from all three resolver paths; throw on cross-clone mismatch (DB rows are preserved — they legitimately belong to the other clone) - Compute canonicalRepoPath once at the top of resolve() - Six new tests in resolver.test.ts covering each guarded path's cross-checkout and same-clone behaviors Fixes coleam00#1183 Fixes coleam00#1188 (part 1 — cross-checkout; part 2 parallel collision deferred to follow-up alongside coleam00#1036) * fix: address PR review — polish, observability, secondary gap, docs Addresses the multi-agent review on coleam00#1206: Code fixes: - worktree.adoption_refused_cross_checkout log event renamed to match CLAUDE.md {domain}.{action}_{state} convention - verifyWorktreeOwnership now preserves err.code and err via { cause } when wrapping fs errors, so classifyIsolationError is robust to Node message format changes - Structured fields (codebaseId, canonicalRepoPath) added to all cross-clone rejection logs for incident debugging - Wrap getCanonicalRepoPath at top of resolve() with classified error instead of letting it propagate as an unclassified crash - Extract assertWorktreeOwnership helper on IsolationResolver — centralizes warn-then-rethrow contract, removes duplication - Dedupe toWorktreePath(existing.working_path) calls in resolver paths - Add code comment on findLinkedIssueEnv explaining why throw-on-first is intentional (user decision — surfaces anomaly instead of masking) Secondary gap closed: - WorktreeProvider.findExisting PR-branch adoption path (findWorktreeByBranch) now also verifies ownership — same class of bug as the main path, just via a different lookup Tests: - 8 new unit tests for verifyWorktreeOwnership in @archon/git (matching pointer, different clone, EISDIR/ENOENT errno preservation, submodule pointer, corrupted .git, trailing-slash normalization, cause chain) - tryBranchAdoption cross-clone test now asserts store.create was never called (symmetry with paths 1+2 asserting updateStatus) - New test for cross-clone rejection in the PR-branch-adoption secondary path in worktree.test.ts Docs: - CHANGELOG.md Unreleased entry for the cross-clone fix series - troubleshooting.md "Worktree Belongs to a Different Clone" section documenting all four new error patterns with resolution steps and pointer to coleam00#1192 for the architectural fix * fix(git): use raw .git pointer in cross-clone error message verifyWorktreeOwnership previously called path.resolve() on the gitdir path before embedding it in the error message. On Windows, resolve() prepends a drive letter to a POSIX-style path (e.g., /other/clone → C:\other\clone), which: 1. Misled users by showing a path that doesn't match what's actually in their .git file 2. Broke a Windows-only test asserting the error contains the literal /other/clone path Compare on resolved paths (correct — normalizes trailing slashes and relative components for the equality check) but display the raw match in the error message (recognizable to the user).
Worktrees created via `git worktree add` do not initialize submodules — monorepo workflows that need submodule content find empty directories. Auto-detect `.gitmodules` and run `git submodule update --init --recursive` after worktree creation; classify failures through the isolation error pipeline. Behavior: - `.gitmodules` absent → skip silently (zero-cost probe, no effect on non-submodule repos) - `.gitmodules` present → run submodule init by default (opt out via `worktree.initSubmodules: false`) - submodule init or `.gitmodules` read failure → throw with classified error including opt-out guidance - Only `ENOENT` on `.gitmodules` is treated as "no submodules"; other access errors (EACCES, EIO) surface as failures to prevent silent empty-dir worktrees Changes: - `packages/isolation/src/providers/worktree.ts` — `initSubmodules()` method + call site in `createWorktree()` - `packages/isolation/src/errors.ts` — collapsed `errorPatterns` + `knownPatterns` into single `ERROR_PATTERNS` source of truth with `known: boolean` per entry; added submodule pattern with opt-out guidance - `packages/isolation/src/types.ts` + `packages/core/src/config/config-types.ts` — new `initSubmodules?: boolean` config option - `packages/docs-web/src/content/docs/reference/configuration.md` — documented the new option and submodule behavior - Tests: default-on, explicit opt-in, explicit opt-out, skip-when-absent, fail-fast on EACCES, fail-fast on git failure, fail-fast on timeout Credit to @halindrome for the original implementation and root-cause mapping across coleam00#1183, coleam00#1187, coleam00#1188, coleam00#1192. Follow-up: coleam00#1192 (codebase identity rearchitect) would retire the cross-clone guard code in `resolver.ts` and `worktree.ts` that coleam00#1198, coleam00#1206 added. Separate PR. Closes coleam00#1187
Summary
Fixes two compounding bugs that cause workflow runs to silently operate on the wrong branch:
archon-implementcommand's branch decision table is ambiguous — AI can adopt unrelated pre-existing branches instead of using its assigned worktreeWorktreeProvider.findExisting()adopts any worktree at the computed path regardless of which clone created it, andcreateNewBranch()silently inherits stale branch commitsChanges
.archon/commands/defaults/archon-implement.md$BASE_BRANCHexplicitlypackages/isolation/src/providers/worktree.tsfindExisting(): verify worktree parent repo matches request before adoptingpackages/isolation/src/providers/worktree.tscreateNewBranch():git branch -fstale orphan branches to intended start-point before checkoutpackages/isolation/src/providers/worktree.test.tsRoot Cause
Prompt: The
archon-implementcommand's Phase 2.2 used a flat table with "On feature branch → Use it" which the AI could interpret as "use any existing feature branch." The other implement commands (archon-fix-issue,archon-implement-issue) already use a proper decision tree withIN WORKTREE?as the first check.Code:
findExisting()checks onlyworktreeExists(path)— two clones of the same remote resolve to the same worktree base directory, so clone B adopts clone A's worktree.createNewBranch()falls back togit worktree add path branch(without-b) when the branch exists, inheriting whatever stale commits were on it.Testing
Fixes #1193
Supersedes #1186 (thanks to @halindrome for the initial framing and scoping — their PR identified the root cause class and explicitly flagged this provider-layer gap as a known limitation, which directly shaped #1198's approach).
Relates to #1188
Summary by CodeRabbit
Documentation
Bug Fixes
Tests