Skip to content

fix(coding-agent): keep vendored rules discovery inside the project root across Windows drives - #568

Open
MoerAI wants to merge 1 commit into
code-yeongyu:mainfrom
MoerAI:fix/rules-finder-cross-drive-project-scope
Open

fix(coding-agent): keep vendored rules discovery inside the project root across Windows drives#568
MoerAI wants to merge 1 commit into
code-yeongyu:mainfrom
MoerAI:fix/rules-finder-cross-drive-project-scope

Conversation

@MoerAI

@MoerAI MoerAI commented Jul 31, 2026

Copy link
Copy Markdown

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/write target 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 any AGENTS.md, CLAUDE.md, CONTEXT.md, .omo/rules, .claude/rules, .cursor/rules or .github/instructions it finds there as project rules. Those rules are then injected as project instructions for a project that does not own them.

Root cause

getWalkDirectories guards the walk with isSameOrChildPath, which classifies containment from the relative() result:

// rules/finder.ts
const childRelativePath = relative(parentPath, childPath);
return childRelativePath === "" || (!childRelativePath.startsWith("..") && !childRelativePath.startsWith("/"));

On Windows, relative() between two different drive roots returns an absolute path rather than a .. chain:

relative("C:\\workspace\\proj", "D:\\other")  ->  "D:\\other"

"D:\\other" starts with neither ".." nor "/", so the containment test returns true. The escape guard never trips, the walk proceeds up the other drive (terminating only at D:\ via the dirname() self-equality check), and every directory on that drive contributes project rule candidates.

The sibling helper in rules/engine.ts already gets this right and uses isAbsolute() for exactly the same test — the finder is the odd one out:

// rules/engine.ts
return childRelativePath === "" || (!childRelativePath.startsWith("..") && !isAbsolute(childRelativePath));

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 in finder.ts. Note this does not close #518 — that report's symptom is the project-root.ts hang already fixed by #432 and shipped in v2026.7.30 (the reporter was on v2026.7.29-6).

Fix

Use isAbsolute() instead of startsWith("/"), matching rules/engine.ts.

POSIX behavior is unchanged: there isAbsolute() and startsWith("/") agree, and relative() between two POSIX paths never yields an absolute result.

File Change
packages/coding-agent/src/core/extensions/builtin/rules/rules/finder.ts isSameOrChildPath rejects an absolute relative() result via isAbsolute(); brief comment on why
packages/coding-agent/src/core/extensions/builtin/rules/changes.md Vendored-adaptation entry per the changes.md contract, with the upstream-propose note
packages/coding-agent/test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.ts Regression test, modelled on the existing 0000-rules-find-project-root-cross-drive.test.ts (mocks node:path to 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 drive D: — no mocks, driving the exported findRuleCandidates API:

projectRoot   = C:\Users\...\qa-proj-c (drive C)
targetFile    = D:\senpi-qa-518\src\file.ts (drive D)
planted rule  = D:\senpi-qa-518\AGENTS.md

project rule candidates returned:
  - D:\senpi-qa-518\AGENTS.md | source = AGENTS.md | distance = 1

RESULT: LEAK - 1 off-project rule(s) collected

Unit-level, the new regression test fails on the pre-fix code for the right reason:

AssertionError: expected [ 'D:\other\AGENTS.md' ] to deeply equal []
- Expected
+ Received
- []
+ [ "D:\\other\\AGENTS.md" ]

Verification (after fix)

Same real cross-drive driver, unchanged inputs:

project rule candidates returned:
  (none)

RESULT: NO LEAK - project scope respected

Test

  • New regression test: test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.ts — RED before, GREEN after.
  • Pre-existing cross-drive test 0000-rules-find-project-root-cross-drive.test.ts still passes.
  • npx vitest --run test/suite/regressions/ test/rules-before-agent-start.test.ts — same failure set as a clean upstream/main baseline 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/main at 9da987f51.


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.

  • Bug Fixes
    • isSameOrChildPath now uses isAbsolute() to reject absolute relative() results, keeping getWalkDirectories scoped to the project root; POSIX behavior unchanged.
    • Added regression test packages/coding-agent/test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.ts and updated packages/coding-agent/src/core/extensions/builtin/rules/changes.md.

Written for commit 687b5d0. Summary will update on new commits.

Review in cubic

…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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +24 to +27
dirname: path.win32.dirname,
join: path.win32.join,
relative: path.win32.relative,
resolve: path.win32.resolve,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

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.

Windows cross-drive project-root traversal fix

1 participant