Skip to content

Follow Windows directory junctions during discovery and file reads - #104

Merged
dacharyc merged 3 commits into
mainfrom
fix/issue-101-windows-junctions
Oct 18, 2026
Merged

dacharyc merged 3 commits into
mainfrom
fix/issue-101-windows-junctions

Conversation

@dacharyc

@dacharyc dacharyc commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Fixes #101.

What was wrong

Since Go 1.23, a Windows directory junction is reported as an irregular file: IsDir() is false and the symlink bit is not set. The discovery switch in discoverSkills matched only directories and symlinks, so a skills directory made of junctions (the reported layout: a real source tree plus a junctioned view of it) was silently reduced to the few entries that were real directories. The reporter saw 2 of 104 skills.

The one-line fix from the issue is not enough on its own. Go's filepath.EvalSymlinks no longer crosses a junction either, and its walker fails with "not a directory" when a junction sits above the last element of a path. A junctioned skill would therefore be found and then have every file in it refused by util.ResolvesWithin.

What changed

  • util.RealPath is EvalSymlinks extended to junctions. On Windows it walks the path and follows each symlink or junction as it is met, in whatever order they chain, restarting after each substitution and bounded at 255 links like EvalSymlinks; the link-free result is then normalized by EvalSymlinks. Elsewhere it is EvalSymlinks.
  • Discovery treats an irregular entry like a symlink and resolves it with RealPath, so a junctioned skill is found, a junctioned alias is reported once, and a junction cycle neither loops nor spends the directory budget.
  • ResolvesWithin uses RealPath for both root and path, so a skill reached through a junction reads normally, and a junction inside a skill that leads outside it is refused, as a symlinked directory already was (SafeReadFile does not guard against symlinked directories #88). Internal links get the same treatment (Should internal-link validation reject symlinks that resolve outside the skill directory? #86).
  • README notes that junctions are followed.

Windows CI

The symlink tests skipped on Windows unconditionally ("symlinks require admin"), which is why this never reached CI. A shared internal/testutil.Symlink helper now skips only when the host actually denies the privilege. The hosted Windows runners are elevated, so the whole symlink suite runs there. testutil.Junction creates junctions, which need no privilege.

New Windows-only tests: junctioned catalog, junction into a grouping directory, junction alias, junction cycle, dangling junction, RealPath through a junction and through mixed symlink/junction chains in both orders, a symlinked skill behind a junction, SafeReadFile on a junctioned root and through an escaping junction, internal links in and out of junctions, and an end-to-end check -o json on a junctioned catalog that asserts every skill is validated with no errors.

Verification

go test -race ./..., go vet for darwin and windows, and golangci-lint run are clean locally. The junction tests only run on Windows, so the Windows CI leg is the verification for them.

No CHANGELOG entry, following the release-commit pattern.

Since Go 1.23, a Windows directory junction is reported as an irregular
file: neither a directory nor a symlink. Discovery matched only those
two kinds, so a skills directory made of junctions, a common Windows
layout, was silently reduced to the few entries that were real
directories. Fixes #101.

Matching the irregular kind is not enough on its own. EvalSymlinks no
longer crosses a junction either, and fails with "not a directory" when
one sits above the last element of a path, so a junctioned skill would
be found and then every file in it refused by the containment check.
util.RealPath expands junctions to their targets, outermost first,
before evaluating symlinks. Discovery and ResolvesWithin use it, so a
junctioned skill reads normally, a junctioned alias or cycle is visited
once, and a junction inside a skill that leads outside it is refused,
as a symlinked directory already was.

Symlink tests used to skip on Windows unconditionally, which is why
none of this reached CI. A shared testutil.Symlink helper skips only
when the host denies the privilege; the hosted Windows runners are
elevated, so the suite now runs there. Junction tests, which need no
privilege, cover discovery, containment, internal links, and a check
run against a junctioned catalog.
…test

The end-to-end junction test copied the valid-skill fixture under new
directory names, so every copy failed the check that a skill's name
matches its directory. The junctioned skills were all discovered and
read, which is what the test is for; only the fixture needed renaming.

Copilot AI 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.

🟡 Changes recommended

Junctions inside symlink targets remain unresolved, causing valid skills to be skipped or rejected.

1 open issue
What changed in this PR

Adds Windows junction support to skill discovery and path containment checks, addressing skipped junctioned skills.

Changes:

  • Introduces junction-aware path resolution.
  • Adds Windows regression and integration tests.
  • Enables symlink tests when privileges permit and documents junction support.
File Description
util/​util.go Uses junction-aware containment checks.
util/​util_windows_test.go Tests junction resolution and safe reads.
util/​util_test.go Enables privilege-aware symlink tests.
util/​realpath.go Adds shared path resolution.
util/​junction_windows.go Expands Windows junction targets.
util/​junction_other.go Preserves non-Windows behavior.
structure/​links_windows_test.go Tests junction link boundaries.
structure/​links_test.go Enables Windows symlink coverage.
skillcheck/​validator.go Follows junctions during discovery.
skillcheck/​validator_test.go Adopts shared symlink helper.
skillcheck/​discovery_windows_test.go Tests junction discovery scenarios.
skill/​skill_test.go Enables Windows symlink rejection test.
README.md Documents junction discovery support.
internal/​testutil/​symlink.go Adds privilege-aware symlink helper.
internal/​testutil/​privilege_windows.go Detects missing symlink privileges.
internal/​testutil/​privilege_other.go Keeps other creation failures fatal.
internal/​testutil/​junction_windows.go Adds junction creation helper.
cmd/​junction_windows_integration_test.go Tests junctioned catalogs end to end.

Review effort: Balanced

Comment thread util/realpath.go Outdated
RealPath expanded junctions first and then called EvalSymlinks, so a
junction met inside a symlink's target was still left to EvalSymlinks,
which fails on it with "not a directory". A catalog entry that is a
symlink to a skill behind a junction was skipped by discovery, and
reads and link checks through such an alias failed. Flagged in review.

Replace the two-phase expansion with one walker on Windows that follows
each symlink or junction as it is met, resolving relative symlink
targets against the link's directory or volume, restarting from the
volume after each substitution, and giving up after 255 links as
EvalSymlinks does. EvalSymlinks then only normalizes the link-free
result. Other platforms keep calling EvalSymlinks directly.

Cover a symlink into a junction, a relative one, a junction to a
symlink, a read through a symlink into a junction, and discovery of a
symlinked skill behind a junction.

Copilot AI 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.

🟢 Approval recommended

No unresolved findings remain, and reported Windows/Linux tests and lint checks passed.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@dacharyc
dacharyc merged commit 8e5fcba into main Oct 18, 2026
4 checks passed
@dacharyc
dacharyc deleted the fix/issue-101-windows-junctions branch October 18, 2026 15:44
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: junctioned skills (ModeIrregular) silently skipped in multi-skill discovery

2 participants