Skip to content

Windows: tracked skills symlink materialises as a plain file without Developer Mode #554

Open

Description

@mogul

Context

skills is a tracked symlink in this repo (mode 120000 → .agents/skills). On a Windows checkout with core.symlinks=false (the default for many Git-for-Windows installs, and unavoidable without SeCreateSymbolicLinkPrivilege), Git materialises it as a plain 14-byte text file containing the target path instead of a directory. skills/ therefore does not exist as a directory, and anything that reads through it fails.

Observed during Windows-host validation of #547: the local run reported on the order of 101 failures in the wider suite whose first cause is this materialisation, not the code under test. The same validation also found that the validator's exec-bit check (scripts/validate_provider_input_contract.py:164) fails on Windows on main — that one is fixed by #547.

Why this is an issue and not a patch

There is no test-level fix that makes skills/ a directory while also keeping it a symlink for POSIX consumers. The options all have costs:

  1. Require Developer Mode (or a policy granting SeCreateSymbolicLinkPrivilege) for Windows contributors, and document it as a prerequisite. Cheapest, but makes "clone and run the suite" fail by default on Windows until the developer flips a global OS setting.
  2. Junction instead of symlink — but a junction cannot be committed to Git as a portable, cross-platform artifact; it would still check out as a file.
  3. Stop tracking skills as a symlink and generate it at bootstrap (e.g. a setup step that creates the link/junction, or a real directory populated from .agents/skills). Removes the failure mode for everyone, but changes the kit layout in both this repo and GSA-TTS/agentic-coding-quickstart, and affects any consumer that expects the tracked symlink.

Please decide

Which of the above is acceptable, and for which platforms? Anything that changes the tracked layout needs an ADR, since it is a repository-shape change rather than a code change.

Interim

Until this is resolved, a Windows contributor needs Developer Mode or core.symlinks=false plus awareness that paths under skills/ will not resolve. A short CONTRIBUTING note is being added alongside this issue.

AI-assisted (OpenCode [deepseek-v4.1-flash]); human review required.

Activity

  1. mogul commented on Oct 29, 2026

    @mogul
    ContributorAuthor

    Related finding: PR CI only runs for PRs whose base is main

    Connecting this to the same Windows/CI-coverage theme, since it came out of the Windows validation work.

    Three workflows in .github/workflows/ gate their pull_request trigger on branches: [main]:

    Workflow PR trigger
    ci.yml pull_request: branches: [main] — also push and merge_group
    frontend-skills-qa.yml pull_request: branches: [main]
    zizmor.yml pull_request: branches: [main]
    pr-lint.yml pull_request: (unfiltered) — runs for any base

    So a PR based on anything other than main gets only pr-lint.

    Observed on #555

    That PR is stacked on fix/validator-windows-exec-bit (#547), because the Windows test fixes cannot be demonstrated on main while #547's exec-bit defect is unfixed. Its head ecba5a5 has exactly one check:

    Conventional Commit Title = success
    

    No Python Lint & Test, no Pattern Validation, no acq-kits Tests. The change is tests-only, so the risk is modest — but a stacked PR landing without the Python suite having run is a gap, and the PR body has to say "CI is authoritative" while CI is in fact not running.

    Why this matters more now

    Stacking is the natural way to land a dependent change here, and the Windows epic produces dependent changes by construction (validator fix → harness fix; CRLF fix → guard fix). A filter that silences CI for exactly those PRs makes the stacked pattern unsafe rather than merely awkward.

    Options

    1. Drop the branches: [main] filter on pull_request for ci.yml / frontend-skills-qa.yml / zizmor.yml. GitHub already scopes to PRs opened against this repository, so the filter buys little; the cost is CI minutes on PRs targeting a non-main base (few, and short-lived).
    2. Keep the filter and require unstacking before merge — rebase onto main once the base PR merges, then let CI run. Honest, but it means the reviewed diff and the CI-verified diff are different commits.
    3. Leave as-is and document it in CONTRIBUTING, so nobody reads a green PR as "tested".

    For contrast, the sibling repo is more permissive: GSA-TTS/agentic-coding-quickstart#667 is stacked on fix/648-msb-ports-crlf and still got 9/9 checks (pre-commit, the bash 3.2 suite, markdown, links, pr-lint). So this is repo-specific configuration, not an inherent property of stacking.

    Recommendation: option 1, with option 3 as the interim note.

    AI-assisted (OpenCode [deepseek-v4.1-flash]); human review required.

  2. mogul commented on Oct 29, 2026

    @mogul
    ContributorAuthor

    One more data point for the options above: without the privilege, ln -s fails silently

    Measured on a default non-elevated Windows host (account in BUILTIN\Administrators, but a filtered Medium-integrity token, so SeCreateSymbolicLinkPrivilege is absent):

    $ whoami /priv | grep -i symlink
    (no output — privilege not held)
    
    $ ln -s target linkout
    ln -s: exit 0
    [ -d ] true (fell back to COPY)          <- exit code says success, result is a copy
    
    $ MSYS=winsymlinks:nativestrict ln -s target linkout
    ln: failed to create symbolic link: Operation not permitted
    

    Two things follow.

    1. ln -s success does not mean a link was created. MSYS/Git Bash falls back to copying unless MSYS=winsymlinks:nativestrict is set. Any script that asserts "link created" by exit status is fooled. Worth auditing any host-runnable script that links with ln -s and checks only $?.

    2. For reference, the kit path is not affected. integrations/isolation/acq-kits/agentic-coding-playbook/files/home/playbook-clone.sh links $HOME/.config/opencode/AGENTS.md, $HOME/.codex/AGENTS.md, $HOME/.claude/CLAUDE.md, $HOME/.copilot/copilot-instructions.md, and each skill into $HOME/.agents/skills/ — but it runs as a hybrid/v1 kit startup command inside the sandbox, so those ln -sfn calls execute against the guest's Linux filesystem. No Windows privilege is involved and no host mounts are used (checked: the only host-side reference to these paths in the quickstart backends is acq.backends/sbx.sh:1389, which runs sh -c via sbx exec in the guest to clear a dangling opencode.jsonc symlink).

    The host-side symlink exposure is therefore limited to the two cases in this issue: git materialising the tracked skills entry, and test fixtures calling os.symlink.

    Consequence for option 1 (require Developer Mode): if that is the chosen direction, the guidance should specify MSYS=winsymlinks:nativestrict for any host-run MSYS shell, so a missing privilege fails loudly instead of degrading into copies that look correct.

    AI-assisted (OpenCode [deepseek-v4.1-flash]); human review required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions