fix(coding-agent): keep vendored rules discovery inside the project root across Windows drives - #568
Conversation
…oot across Windows drives
`isSameOrChildPath` in the vendored rules finder rejected an escaping
`relative()` result with `startsWith("/")`. On Windows, `relative()` between
two different drive roots returns an absolute path — `relative("C:\\proj",
"D:\\other")` is `"D:\\other"` — which starts with neither ".." nor "/", so the
containment test accepted it.
`getWalkDirectories` therefore walked the other drive instead of falling back
to the project root, and `findProjectCandidates` collected `AGENTS.md`,
`CLAUDE.md`, `CONTEXT.md` and `.claude/rules` from that unrelated drive as
*project* rules, injecting them into the model context for a project that does
not own them.
The sibling helper in `rules/engine.ts` already uses `isAbsolute()` for the
same test; this aligns the finder with it. POSIX behavior is unchanged, since
`isAbsolute()` and `startsWith("/")` agree there.
Verified on Windows 11 with a real second drive: with a rule planted at
`D:\senpi-qa-518\AGENTS.md` and the project root on `C:`, discovery returned
that file as an `AGENTS.md` project rule at distance 1 before the change and
returns no candidates after it.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 687b5d0adb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| dirname: path.win32.dirname, | ||
| join: path.win32.join, | ||
| relative: path.win32.relative, | ||
| resolve: path.win32.resolve, |
There was a problem hiding this comment.
Mock the Windows
isAbsolute implementation
On Linux and macOS runners, this mock replaces relative with its Win32 variant but leaves isAbsolute as the host POSIX implementation. The finder therefore evaluates isAbsolute("D:\\other") as false, still treats the cross-drive target as a child, and returns the mocked D:\\other\\AGENTS.md, causing the new assertion to fail. Override isAbsolute with path.win32.isAbsolute so this regression runs successfully on every supported CI platform.
AGENTS.md reference: packages/coding-agent/test/AGENTS.md:L43-L46
Useful? React with 👍 / 👎.
Summary
On Windows, project rule discovery can escape the project root and pull rule files from an unrelated drive into the model context.
When a
read/edit/writetarget lives on a different drive than the project root, the vendored rules finder treats that target as being inside the project, walks the other drive, and collects anyAGENTS.md,CLAUDE.md,CONTEXT.md,.omo/rules,.claude/rules,.cursor/rulesor.github/instructionsit finds there as project rules. Those rules are then injected as project instructions for a project that does not own them.Root cause
getWalkDirectoriesguards the walk withisSameOrChildPath, which classifies containment from therelative()result:On Windows,
relative()between two different drive roots returns an absolute path rather than a..chain:"D:\\other"starts with neither".."nor"/", so the containment test returnstrue. The escape guard never trips, the walk proceeds up the other drive (terminating only atD:\via thedirname()self-equality check), and every directory on that drive contributes project rule candidates.The sibling helper in
rules/engine.tsalready gets this right and usesisAbsolute()for exactly the same test — the finder is the odd one out:This is the same bug class as #432 (Windows cross-drive traversal in this module), but a different site and a different symptom: #432 was a hang in
project-root.ts, this is a silent project-scope escape infinder.ts. Note this does not close #518 — that report's symptom is theproject-root.tshang already fixed by #432 and shipped inv2026.7.30(the reporter was onv2026.7.29-6).Fix
Use
isAbsolute()instead ofstartsWith("/"), matchingrules/engine.ts.POSIX behavior is unchanged: there
isAbsolute()andstartsWith("/")agree, andrelative()between two POSIX paths never yields an absolute result.packages/coding-agent/src/core/extensions/builtin/rules/rules/finder.tsisSameOrChildPathrejects an absoluterelative()result viaisAbsolute(); brief comment on whypackages/coding-agent/src/core/extensions/builtin/rules/changes.mdchanges.mdcontract, with the upstream-propose notepackages/coding-agent/test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.ts0000-rules-find-project-root-cross-drive.test.ts(mocksnode:pathto win32 so it runs on every CI platform)Reproduction (before fix)
Real Windows 11 machine, project root on
C:, rule planted on a real second driveD:— no mocks, driving the exportedfindRuleCandidatesAPI:Unit-level, the new regression test fails on the pre-fix code for the right reason:
Verification (after fix)
Same real cross-drive driver, unchanged inputs:
Test
test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.ts— RED before, GREEN after.0000-rules-find-project-root-cross-drive.test.tsstill passes.npx vitest --run test/suite/regressions/ test/rules-before-agent-start.test.ts— same failure set as a cleanupstream/mainbaseline on this Windows box (9 files: multi-session-theme-init, fswatch-error-crash, replaced-session-context, find-path-glob, claude-sdk-oauth-installed-sdk-hook-stop, bun-launcher-self-update, codemode-builtin-dedupe, inspector-vm-import-crash, todo-12-stale-lock-owner). All are pre-existing and unrelated to this change; they flake between 8 and 9 files across runs on a clean tree.npm run check— clean (it runs as the pre-commit hook: biome + pinned-deps + ts-imports + shrinkwrap + install-lock + tsgo + browser-smoke + web-ui).Rebased on
upstream/mainat9da987f51.Summary by cubic
Fixes Windows cross-drive containment in vendored rules discovery so project rules stay inside the project root. Prevents unrelated drive files from being injected into the model context.
isSameOrChildPathnow usesisAbsolute()to reject absoluterelative()results, keepinggetWalkDirectoriesscoped to the project root; POSIX behavior unchanged.packages/coding-agent/test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.tsand updatedpackages/coding-agent/src/core/extensions/builtin/rules/changes.md.Written for commit 687b5d0. Summary will update on new commits.