Skip to content

fix(windows): canonicalize permission path variants#493

Merged
Astro-Han merged 4 commits intodevfrom
codex/fix-i427-windows-path-canonicalization
May 7, 2026
Merged

fix(windows): canonicalize permission path variants#493
Astro-Han merged 4 commits intodevfrom
codex/fix-i427-windows-path-canonicalization

Conversation

@Astro-Han
Copy link
Copy Markdown
Owner

@Astro-Han Astro-Han commented May 7, 2026

Summary

Canonicalizes the remaining Windows permission path variants for #427 correctness and makes guarded tools use the same canonical path for permission checks and actual filesystem/LSP/ripgrep operations.

Changes included:

  • Add a Windows canonicalizer path that folds extended-length drive paths, extended UNC paths, rooted-but-driveless future targets with an explicit base drive, and ambiguous rooted-but-driveless existing paths.
  • Return the canonical target from assertExternalDirectoryEffect.
  • Use that canonical target across read, write, edit, apply_patch, glob, grep, and lsp boundaries.

Why

#431 fixed the existing rooted-but-driveless case, but #427 still had correctness gaps where Windows path variants could produce different permission patterns or let the permission path diverge from the actual tool path. That means a user could approve one spelling with always and still be prompted again, or approve coverage for a path that is not the one the tool later uses.

This PR fixes the correctness portion at the path contract and tool boundary. It intentionally does not implement drive-root probe caching.

Related Issue

Addresses the correctness portion of #427.

Does not close #427 unless the remaining drive-root probe caching follow-up is split into a separate issue.

Human Review Status

Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.

Review Focus

  • Windows canonicalizer behavior for \\?\D:\..., \\?\UNC\server\share\..., non-existing rooted-but-driveless targets, and ambiguous multi-drive matches.
  • Whether each guarded tool now uses the canonical path consistently after assertExternalDirectoryEffect returns.
  • The intentional scope boundary: correctness only, no drive-root probe caching or cache invalidation in this PR.

Risk Notes

  • Windows-only behavior change for path normalization and external-directory permission paths.
  • Ambiguous rooted-but-driveless existing paths now fail with a clear error instead of silently picking the first drive root.
  • Real network UNC share behavior still needs Windows environment validation; deterministic unit tests cover prefix folding and path-contract behavior without requiring a live share.
  • Drive-root probe caching remains out of scope.

How To Verify

Core filesystem tests: bun --cwd packages/core test test/filesystem/normalize-path.test.ts -> 6 pass / 7 skip / 0 fail
Tool boundary tests: bun --cwd packages/opencode test test/tool/external-directory.test.ts test/tool/read.test.ts test/tool/write.test.ts test/tool/edit.test.ts test/tool/apply_patch.test.ts test/tool/glob.test.ts test/tool/grep.test.ts test/tool/lsp.test.ts -> 127 pass / 0 fail
Core typecheck: (cd packages/core && bun run typecheck) -> exit 0
Opencode typecheck: (cd packages/opencode && bun run typecheck) -> exit 0
Diff check: git diff --check -> exit 0

Screenshots or Recordings

None. No visible UI changes.

Checklist

  • Human review status is stated above as pending, approved, or not required
  • I linked the related issue, or stated why there is no issue
  • This PR has type, primary area, and priority labels, or I requested maintainer labeling
  • I described the review focus and any meaningful risks
  • I listed the relevant verification steps and the key result for each
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant
  • I reviewed the final diff for unrelated changes and suspicious dependency changes
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • Refactor

    • Improved cross-platform file path handling with enhanced Windows path normalization.
    • Refactored file operation tools (read, write, edit, search, patch) to better validate and resolve file paths across platforms.
    • Enhanced external directory validation with improved path resolution.
  • Tests

    • Added comprehensive cross-platform path normalization test coverage, including extended-length and UNC path support.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 8 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 98c99092-5585-4fb4-8474-af5314406551

📥 Commits

Reviewing files that changed from the base of the PR and between 992c7b0 and 912114c.

📒 Files selected for processing (4)
  • packages/core/src/filesystem.ts
  • packages/core/test/filesystem/normalize-path.test.ts
  • packages/opencode/src/tool/grep.ts
  • packages/opencode/test/tool/external-directory.test.ts
📝 Walkthrough

Walkthrough

This PR refactors Windows path normalization to resolve variant spellings into canonical forms before permission validation. It introduces WindowsPathOptions configuration, exports public path resolution APIs, updates all opencode tools to use normalized paths for validation, and adds cross-platform tests.

Changes

Windows Path Canonicalization and Tool Integration

Layer / File(s) Summary
Windows Path Options Contract
packages/core/src/filesystem.ts
New WindowsPathOptions type with base, driveRoots, and exists callback enables configurable drive-root discovery and path existence checks.
Exported Path APIs
packages/core/src/filesystem.ts
normalizePath, normalizePathPattern, and resolve now accept optional WindowsPathOptions and return platform-appropriate results using the new configuration.
Shell Input Normalization
packages/core/src/filesystem.ts
New normalizeWindowsShellInput helper converts POSIX-style drive prefixes (/c/, /mnt/c/, /cygdrive/c/) to uppercase Windows drive-letter forms.
Windows Normalization Internals
packages/core/src/filesystem.ts
Refactored Windows path handling strips extended-length prefixes, probes rooted-driveless paths against configurable drive roots, detects ambiguity when multiple matches exist, and falls back to explicit base-drive or win32.resolve normalization.
External Directory Effect
packages/opencode/src/tool/external-directory.ts
assertExternalDirectoryEffect now normalizes Windows paths against instance directory, constructs globs with normalized base, and returns the normalized full path for use by calling tools.
Tool Path Validation
packages/opencode/src/tool/apply_patch.ts, edit.ts, glob.ts, grep.ts, lsp.ts, read.ts, write.ts
All tools capture and use the normalized path returned from assertExternalDirectoryEffect; grep and read apply Windows-specific base directory normalization.
Filesystem Tests
packages/core/test/filesystem/normalize-path.test.ts
New withWin32Platform helper simulates Windows in tests; added coverage for extended-length path folding, rooted-driveless resolution with base drive, uppercase drive-letter handling, and ambiguity detection.
Tool Tests
packages/opencode/test/tool/external-directory.test.ts
New withWin32Platform helper and tests verify assertExternalDirectory returns canonical Windows paths with matching permission metadata.

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Astro-Han/pawwork#431: Both PRs address Windows path canonicalization by refactoring packages/core/src/filesystem.ts drive-root probing logic for rooted-but-driveless paths.

Suggested labels

bug, windows, P1

Poem

🐰 Windows paths now dance in harmony,
With options configured, base and driveRoots spree!
Extended prefixes fold away,
Each variant route finds canonical day—
Permissions unified, no more askings thrice! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: canonicalizing Windows permission path variants, which directly addresses the PR objectives.
Description check ✅ Passed The description comprehensively covers all required template sections: summary, why, related issue, review focus, risk notes, verification steps, and completed checklist.
Linked Issues check ✅ Passed The PR addresses the remaining-scope items from #427: folding extended-length drive paths, extended UNC paths, rooted-but-driveless targets with explicit base drive, and ambiguous multi-drive matches that now fail with clear errors.
Out of Scope Changes check ✅ Passed All changes align with stated scope: Windows path canonicalization at the tool boundary, permission path consistency, and tool contract updates. Drive-root probe caching was intentionally deferred as noted.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-i427-windows-path-canonicalization

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request enhances Windows path normalization by introducing normalizeWindowsPath and WindowsPathOptions, which provide support for extended-length paths and improved resolution of rooted-driveless paths. These improvements are integrated into various tools to ensure consistent path handling. The review feedback suggests reducing code duplication by reusing the existing windowsPath function, removing redundant normalization calls, and making platform-dependent logic injectable to facilitate cross-platform testing.

Comment thread packages/core/src/filesystem.ts Outdated
Comment thread packages/core/src/filesystem.ts
Comment thread packages/core/src/filesystem.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/opencode/src/tool/grep.ts (1)

55-74: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

assertExternalDirectoryEffect return value is not captured — ripgrep uses the pre-canonical path

Every other tool changed in this PR (read, write, edit, glob, lsp, apply_patch) captures the canonical path returned by assertExternalDirectoryEffect and uses it for downstream operations. grep is the only tool that still ignores the return value. As a result, cwd and file (and therefore the ripgrep call) are computed from the unnormalized search, which defeats the Windows canonicalization intent of this PR.

🐛 Proposed fix
-          yield* assertExternalDirectoryEffect(ctx, search, {
+          const canonSearch = (yield* assertExternalDirectoryEffect(ctx, search, {
             kind: info?.type === "Directory" ? "directory" : "file",
-          })
+          })) ?? search
-          const cwd = info?.type === "Directory" ? search : path.dirname(search)
-          const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
+          const cwd = info?.type === "Directory" ? canonSearch : path.dirname(canonSearch)
+          const file = info?.type === "Directory" ? undefined : [path.relative(cwd, canonSearch)]

Then update the rg.search call to use the already-derived cwd/file (no change needed there since they now derive from canonSearch).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/tool/grep.ts` around lines 55 - 74, The call to
assertExternalDirectoryEffect in grep.ts currently ignores its return value so
cwd/file are computed from the pre-canonical search path; capture the returned
canonical path (e.g., const canonSearch = yield*
assertExternalDirectoryEffect(...)) and then compute cwd and file from
canonSearch (use path.dirname/canonSearch and path.relative as done elsewhere),
and pass those derived cwd/file into rg.search so ripgrep runs against the
canonicalized path.
packages/opencode/src/tool/external-directory.ts (1)

21-44: ⚠️ Potential issue | 🟠 Major

Fix grep.ts to capture and use the normalized path from assertExternalDirectoryEffect.

At line 64, grep.ts calls assertExternalDirectoryEffect but discards its return value, continuing with the pre-normalization search for downstream rg.search() operations. This re-introduces the variant-spelling bug this PR fixes.

Change:

yield* assertExternalDirectoryEffect(ctx, search, {
  kind: info?.type === "Directory" ? "directory" : "file",
})

To:

search = (yield* assertExternalDirectoryEffect(ctx, search, {
  kind: info?.type === "Directory" ? "directory" : "file",
})) ?? search

All other callers (write.ts, glob.ts, read.ts, apply_patch.ts, lsp.ts, edit.ts) correctly capture and use the return value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/tool/external-directory.ts` around lines 21 - 44, The
call to assertExternalDirectoryEffect in grep.ts currently discards its return
value, so downstream rg.search() is still using the un-normalized search path;
change the call to capture and use the returned normalized path by reassigning
search to the effect's result (fallback to the original search if the effect
returns undefined) — this references the function assertExternalDirectoryEffect
and the local variable search used before invoking rg.search().
🧹 Nitpick comments (3)
packages/core/src/filesystem.ts (2)

257-269: 💤 Low value

Ambiguity check ignores caller-supplied drive root duplicates.

resolveRootedWindowsVariant accepts options.driveRoots verbatim. If callers pass duplicates (e.g. ["C:\\", "c:\\"]), the same on-disk file can be probed twice and pushed to matches twice, triggering the ambiguity throw on a path that is not actually ambiguous. The internal windowsDriveRoots() already de-dupes via seen, so the gap only exists for caller-supplied roots. A cheap normalization (new Set(roots.map(r => r.toUpperCase()))) before the loop closes this off and keeps the contract symmetric with the internal source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/filesystem.ts` around lines 257 - 269,
resolveRootedWindowsVariant can push duplicate candidate paths when
caller-supplied options.driveRoots contains case-duplicates (e.g. "C:\\" and
"c:\\") causing a false ambiguity; normalize and de-duplicate the driveRoots
before iterating (e.g. derive a set from options.driveRoots or
windowsDriveRoots() using a case-normalization like toUpperCase()) so the loop
over roots only probes unique normalized roots and the matches array cannot
contain duplicate identical candidates; update references to options.driveRoots
and windowsDriveRoots() in resolveRootedWindowsVariant accordingly.

243-277: 💤 Low value

Approach LGTM; the ambiguity throw is a plain Error.

The pipeline ordering is correct (normalizeWindowsShellInput only matches /-prefixed forms, stripExtendedLengthPrefix only matches \\?\ forms, so they don't fight), isRootedDriveless correctly excludes UNC via the negative lookahead, and the existing-match → base-bound → cwd-bound fallback chain matches the PR objective.

One small note: throw new Error("Ambiguous Windows path …") here is a plain Error, not an AppFileSystem.FileSystemError. Since normalizePath/normalizeWindowsPath are now invoked from Effect.fn callers in the opencode tool layer (see comment on external-directory.ts), a typed error class would round-trip more cleanly through Effect's error channel. Not blocking — the throw site itself is correct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/filesystem.ts` around lines 243 - 277, In
resolveRootedWindowsVariant, replace the plain throw new Error(...) with a typed
filesystem error (e.g., throw new AppFileSystem.FileSystemError(...)) so callers
(including Effect.fn in external-directory.ts) can round-trip the error through
Effect's error channel; update the resolveRootedWindowsVariant function to
construct the FileSystemError with the same descriptive message (including the
ambiguous path p) and add the necessary import for AppFileSystem.FileSystemError
at the top of the module.
packages/opencode/src/tool/external-directory.ts (1)

24-32: ⚡ Quick win

Ambiguous-path throw becomes a defect inside Effect.fn.

AppFileSystem.normalizePath/normalizePathPattern throw a plain Error("Ambiguous Windows path …") when multiple drive roots match. Because these are synchronous throws inside Effect.fn, the failure surfaces as an unchecked defect and the tool effect dies rather than producing a clean, user-visible error — which is the new failure mode the PR explicitly introduces ("ambiguous multi-drive matches now fail with a clear error instead of picking the first drive"). Consider trapping the throw so it crosses the Effect boundary as a typed failure on ctx.ask / the tool's error channel.

♻️ Sketch: convert the sync throw into a typed Effect failure
-  const full = process.platform === "win32" ? AppFileSystem.normalizePath(target, { base: ins.directory }) : target
+  const full =
+    process.platform === "win32"
+      ? yield* Effect.try({
+          try: () => AppFileSystem.normalizePath(target, { base: ins.directory }),
+          catch: (cause) => new SomeToolError({ method: "normalizePath", cause }),
+        })
+      : target
   if (options?.bypass) return full
   if (Instance.containsPath(full, ins)) return full

   const kind = options?.kind ?? "file"
   const dir = kind === "directory" ? full : path.dirname(full)
-  const glob =
-    process.platform === "win32"
-      ? AppFileSystem.normalizePathPattern(path.join(dir, "*"), { base: ins.directory })
-      : path.join(dir, "*").replaceAll("\\", "/")
+  const glob =
+    process.platform === "win32"
+      ? yield* Effect.try({
+          try: () => AppFileSystem.normalizePathPattern(path.join(dir, "*"), { base: ins.directory }),
+          catch: (cause) => new SomeToolError({ method: "normalizePathPattern", cause }),
+        })
+      : path.join(dir, "*").replaceAll("\\", "/")

As per coding guidelines: "Use Schema.TaggedErrorClass for typed errors in Effect schemas".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/tool/external-directory.ts` around lines 24 - 32, The
calls to AppFileSystem.normalizePath and AppFileSystem.normalizePathPattern can
synchronously throw an "Ambiguous Windows path" Error inside Effect.fn causing
an unchecked defect; wrap those calls so any thrown Error is caught and
converted into a typed Effect failure (use a Schema.TaggedErrorClass, e.g.
AmbiguousPathError) and return Effect.fail (or otherwise surface via the tool's
ctx.ask/error channel) instead of letting the throw escape; update the logic
around the code that computes full and glob (referencing
AppFileSystem.normalizePath, AppFileSystem.normalizePathPattern, the Effect.fn
wrapper around this code, and the surrounding flow that uses
Instance.containsPath and options?.bypass) to catch the sync throw and map it to
the tagged error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/test/tool/external-directory.test.ts`:
- Around line 25-31: The withWin32Platform helper currently overrides
process.platform using Object.defineProperty without setting configurable, which
prevents restoring and throws when redefining; update withWin32Platform to
capture the original property descriptor (via
Object.getOwnPropertyDescriptor(process, "platform")), then call
Object.defineProperty(process, "platform", { value: "win32", configurable: true
}) and in the finally block restore the original descriptor (or the original
value and flags) using the saved descriptor to reliably revert process.platform
without errors.

---

Outside diff comments:
In `@packages/opencode/src/tool/external-directory.ts`:
- Around line 21-44: The call to assertExternalDirectoryEffect in grep.ts
currently discards its return value, so downstream rg.search() is still using
the un-normalized search path; change the call to capture and use the returned
normalized path by reassigning search to the effect's result (fallback to the
original search if the effect returns undefined) — this references the function
assertExternalDirectoryEffect and the local variable search used before invoking
rg.search().

In `@packages/opencode/src/tool/grep.ts`:
- Around line 55-74: The call to assertExternalDirectoryEffect in grep.ts
currently ignores its return value so cwd/file are computed from the
pre-canonical search path; capture the returned canonical path (e.g., const
canonSearch = yield* assertExternalDirectoryEffect(...)) and then compute cwd
and file from canonSearch (use path.dirname/canonSearch and path.relative as
done elsewhere), and pass those derived cwd/file into rg.search so ripgrep runs
against the canonicalized path.

---

Nitpick comments:
In `@packages/core/src/filesystem.ts`:
- Around line 257-269: resolveRootedWindowsVariant can push duplicate candidate
paths when caller-supplied options.driveRoots contains case-duplicates (e.g.
"C:\\" and "c:\\") causing a false ambiguity; normalize and de-duplicate the
driveRoots before iterating (e.g. derive a set from options.driveRoots or
windowsDriveRoots() using a case-normalization like toUpperCase()) so the loop
over roots only probes unique normalized roots and the matches array cannot
contain duplicate identical candidates; update references to options.driveRoots
and windowsDriveRoots() in resolveRootedWindowsVariant accordingly.
- Around line 243-277: In resolveRootedWindowsVariant, replace the plain throw
new Error(...) with a typed filesystem error (e.g., throw new
AppFileSystem.FileSystemError(...)) so callers (including Effect.fn in
external-directory.ts) can round-trip the error through Effect's error channel;
update the resolveRootedWindowsVariant function to construct the FileSystemError
with the same descriptive message (including the ambiguous path p) and add the
necessary import for AppFileSystem.FileSystemError at the top of the module.

In `@packages/opencode/src/tool/external-directory.ts`:
- Around line 24-32: The calls to AppFileSystem.normalizePath and
AppFileSystem.normalizePathPattern can synchronously throw an "Ambiguous Windows
path" Error inside Effect.fn causing an unchecked defect; wrap those calls so
any thrown Error is caught and converted into a typed Effect failure (use a
Schema.TaggedErrorClass, e.g. AmbiguousPathError) and return Effect.fail (or
otherwise surface via the tool's ctx.ask/error channel) instead of letting the
throw escape; update the logic around the code that computes full and glob
(referencing AppFileSystem.normalizePath, AppFileSystem.normalizePathPattern,
the Effect.fn wrapper around this code, and the surrounding flow that uses
Instance.containsPath and options?.bypass) to catch the sync throw and map it to
the tagged error.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eb12a4f8-3dad-4dc2-ba7b-a665fa903d25

📥 Commits

Reviewing files that changed from the base of the PR and between 6889e01 and 992c7b0.

📒 Files selected for processing (11)
  • packages/core/src/filesystem.ts
  • packages/core/test/filesystem/normalize-path.test.ts
  • packages/opencode/src/tool/apply_patch.ts
  • packages/opencode/src/tool/edit.ts
  • packages/opencode/src/tool/external-directory.ts
  • packages/opencode/src/tool/glob.ts
  • packages/opencode/src/tool/grep.ts
  • packages/opencode/src/tool/lsp.ts
  • packages/opencode/src/tool/read.ts
  • packages/opencode/src/tool/write.ts
  • packages/opencode/test/tool/external-directory.test.ts

Comment thread packages/opencode/test/tool/external-directory.test.ts
@Astro-Han
Copy link
Copy Markdown
Owner Author

Follow-up for the new review round:

  • Fixed the process.platform test helper restore issue in both affected test helpers.
  • Fixed grep to keep using the canonical path returned by assertExternalDirectoryEffect before deriving the ripgrep target.
  • Fixed caller-supplied Windows drive roots so duplicate roots do not false-trigger ambiguity, with a regression test.

I did not fold the typed-error/docstring suggestions into this PR because they are not required for #427 correctness and would widen the error-contract surface beyond this path-canonicalization fix.

@Astro-Han Astro-Han merged commit 504ab65 into dev May 7, 2026
20 checks passed
@Astro-Han Astro-Han deleted the codex/fix-i427-windows-path-canonicalization branch May 7, 2026 12:25
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.

[Bug] external_directory and read permissions don't fully canonicalize Windows path variants

1 participant