Skip to content

feat: Windows 11 support — Claude Code (#46)#73

Open
Cannon07 wants to merge 12 commits into
mainfrom
feat/windows-46
Open

feat: Windows 11 support — Claude Code (#46)#73
Cannon07 wants to merge 12 commits into
mainfrom
feat/windows-46

Conversation

@Cannon07
Copy link
Copy Markdown
Owner

@Cannon07 Cannon07 commented Jun 1, 2026

Closes the first slice of #46: Windows 11 support for the Claude Code backend, end-to-end. Built on the in-process Lua orchestration from #47 — the only OS-specific surface is the thin shim + discovery layer, now mirrored in PowerShell.

What's in this PR

  • Architecture: ADR-0007 (shared PowerShell shim; all agents reuse it; supersedes ADR-0006) + CONTEXT.md term updates (Hook entry, Socket discovery, Pidfile).
  • OS-agnostic groundwork: %LOCALAPPDATA% pidfile base on Windows; stem-based hook-marker matching across backends; chmod gated behind has("unix"); CODEX_HOME honoured; per-OS healthcheck (jq vs PowerShell).
  • PowerShell shim layer: bin/nvim-socket.ps1 (named-pipe discovery: env var → pidfile → pipe enumeration → --remote-expr probe) and bin/nvim-call.ps1 (RPC; verbatim payload splice, forward-slashed tempfile).
  • Claude Code shims + installer: backends/claudecode/code-{preview,close}-diff.ps1; installer writes powershell -NoProfile -ExecutionPolicy Bypass -File <path>.ps1 on Windows.
  • CI: a windows-latest job (per-file plenary.busted.run, since PlenaryBustedDirectory hangs headless on Windows; + a PowerShell 5.1 shim syntax check).

Validation

  • macOS/Linux: full suite green (Lua specs + bash E2E), unregressed.
  • Real Windows 11 box (PowerShell 5.1 + nvim 0.11.2): discovery, RPC round-trip, the shim opening a real preview, and live Claude Code edits across projects all pass. Claude Code shell-executes the command string on Windows — no .cmd trampoline needed.
  • Windows CI: green.

Two Windows-only bugs surfaced during validation and are fixed here: --headless is mandatory on nvim --server --remote-expr (else it starts a TUI / false-positive probe), and PowerShell 5.1 strips double quotes from the --remote-expr value (fixed via single-quoted Vimscript + Lua [[...]] literals). Both are documented in the .ps1 comments.

Scope & known limitations

  • Windows support is Claude Code only in this PR. Codex / Copilot / OpenCode shims are follow-ups.
  • Bash change-indicators on Windows are not yet supported (bash_detect is Unix-path-only). The core preview path (Edit/Write/MultiEdit) works; the bash_detect spec + one bash_modified case are skipped on Windows pending that work.
  • The other backends' installers are not yet guarded on Windows — a Windows user who runs :CodePreviewInstall{Codex,CopilotCli,OpenCode}Hooks would write a hook that can't run. Worth deciding whether to add a "not yet supported on Windows" guard before this merges (see review thread).

Remaining rollout (follow-up PRs)

  1. Codex / Copilot / OpenCode .ps1 shims + installer wiring (+ confirm each agent's command-field invocation semantics on Windows).
  2. bash_detect.lua Windows-path awareness + portable specs.
  3. health.lua per-OS script-executability checks (still assume .sh + chmod).
  4. README Windows setup section.

🤖 Generated with Claude Code

Cannon07 and others added 12 commits June 1, 2026 00:27
Captures the design decisions from the Windows-support grilling session:
- ADR-0007: shared PowerShell shim, all agents (incl. OpenCode) reuse it;
  supersedes ADR-0006's prediction that OpenCode would go shim-free in TS.
- CONTEXT.md: Hook entry (per-OS, PowerShell 5.1 floor), Socket discovery
  (pipe-enumeration fallback), Pidfile (%LOCALAPPDATA% base on Windows).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lands the cross-platform-safe portion of Windows support, ahead of the
PowerShell shim layer. All changes are either gated behind has("win32")
(dead code on Unix) or covered by the existing Unix E2E tests:

- pidfile.lua: write the pidfile under %LOCALAPPDATA% on Windows; the
  Unix XDG/$HOME formula yields a driveless path there. Comment explains
  why we don't use stdpath() (the shim reader can't call it).
- backends/codex.lua, copilot.lua: match installed hook entries by
  adapter-script *stem* (code-preview-diff / code-close-diff) instead of
  the full .sh fragment, so detection works for the .ps1 counterparts.
- backends/codex.lua, copilot.lua: gate chmod behind has("unix") — it's a
  no-op on Windows, where the command invokes the interpreter explicitly.
- backends/codex.lua: honour $CODEX_HOME in global_config_path().
- health.lua: report PowerShell presence on Windows in place of the jq
  check (the .ps1 shims use native ConvertFrom-Json).

Verified on macOS: Lua specs (110), codex E2E (22), copilot E2E (15) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First end-to-end Windows path: discovery + RPC primitives plus the Claude
Code shims and installer wiring. Mirrors the .sh layer one-for-one.

- bin/nvim-socket.ps1: named-pipe discovery (env var → pidfile under
  %LOCALAPPDATA% → pipe enumeration), with the --remote-expr responsiveness
  probe. No is-socket precheck (no reliable pipe existence test).
- bin/nvim-call.ps1: RPC over a named pipe. Writes the args JSON to a %TEMP%
  tempfile VERBATIM (no ConvertTo-Json round-trip — ADR-0007 depth invariant)
  and forward-slashes the tempfile path before splicing it into the luaeval
  source (Windows backslashes are Lua escapes).
- backends/claudecode/code-{preview,close}-diff.ps1: the shims; abstain
  (exit 0) on any failure.
- claudecode.lua: on Windows, write `powershell -NoProfile -ExecutionPolicy
  Bypass -File <path>.ps1` into settings and resolve .ps1 paths. Unix path
  unchanged (verified: claudecode E2E 15/15, Lua specs green on macOS).

UNVALIDATED ON WINDOWS — drafted on macOS. Two spike items to confirm on a
real box: Claude Code's invocation of the command field, and PS 5.1
--remote-expr argument quoting (see the comment in nvim-call.ps1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures in-flight state not in the ADRs/commits: the validation ladder
(rungs 0-4) to run on a real Windows box, the two spike items, the held
rollout, and the invariants to preserve. Lets a fresh Claude Code session
on the Windows machine pick up after `git pull`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran the ADR-0007 spike/validation ladder under Windows PowerShell 5.1
(the floor the installed hook command actually uses) against a live
Neovim, and fixed three real bugs the prior macOS-only session could not
catch:

- nvim-socket.ps1 / nvim-call.ps1: add `--headless`. Without it,
  `nvim --server X --remote-expr Y` starts a local TUI instead of acting
  as a pure remote client — stdout came back as terminal escape codes
  with no result, and the responsiveness probe exited 0 even against a
  dead pipe (false positive that would accept stale pidfiles). With
  --headless a dead server correctly yields exit 2.

- nvim-call.ps1: eliminate double quotes from the --remote-expr value.
  PowerShell 5.1 lacks PSNativeCommandArgumentPassing and strips the
  embedded quotes in luaeval("..."), so nvim parsed require(...) as
  Vimscript -> E117. Switched to a single-quoted Vimscript body with Lua
  long-bracket [[...]] literals (zero quote chars to mangle); equally
  correct under pwsh 7.

- pre_tool/init.lua: tmpdir() fell back to "/tmp" on Windows (no $TMPDIR
  there), so every diff tempfile write hit a nonexistent C:\tmp and the
  preview silently failed. Added a Windows branch (TMP/TEMP, forward-
  slashed); the Unix branch is left byte-identical.

- log.lua: normalise the debug-log path with vim.fs.normalize so Windows
  no longer produces a mixed-separator string (...\nvim-data/code-preview.log).
  No-op on Unix; same physical file on Windows.

Validated end-to-end: socket discovery, RPC round-trip, the claudecode
shim opening a real preview, and live Claude Code edits across projects.
Debug logging confirmed working identically to macOS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite the in-flight handoff to reflect that the validation ladder has
been run on a real Windows box: records this session's four fixes
(commit 69ff052), the Rung 0-4 results (incl. Claude Code shell-executing
the hook command with no .cmd trampoline needed), Windows gotchas, and
the updated remaining-rollout list (note: PlenaryBustedDirectory hangs
headless on Windows; bash_detect is Unix-path-only). Original inbound
handoff preserved in git history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a separate windows-test job (the Unix matrix strategy doesn't fit):
- Lua specs run per-file via PlenaryBustedFile (PlenaryBustedDirectory hangs
  headless on Windows), excluding the Unix-path-only bash_detect spec until
  it's ported (handoff item 3).
- A PowerShell 5.1 syntax/load check of the .ps1 shims (shell: powershell is
  Windows PowerShell 5.1 — the floor the installed hook command uses).
- nvim pinned to v0.11.2 (the version the claudecode slice was validated on).

Adds workflow_dispatch, and TEMPORARILY triggers on feat/windows-46 so the
Windows job can be exercised on the branch before merge (remove before merge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PlenaryBustedFile is nargs=1 — passing {minimal_init=...} glued the opts into
the filename ("cannot open <path> {...}"), and even without opts it spawns a
child nvim that wouldn't load tests/minimal_init.lua (missing plugin rtp).
Call plenary.busted.run() in-process instead: nvim is already started with
-u tests/minimal_init.lua, and busted.run sets the process exit code via :cq,
so we detect failures from $LASTEXITCODE rather than parsing ANSI output.

Verified the invocation + exit codes on macOS (pass=0, fail=2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "redirect to existing file marks bash_modified" case needs to create a
/tmp file on disk (io.open fails on Windows) and have bash_detect resolve a
Windows path, which is Unix-path-only today (handoff item 3). Mark it pending
on Windows so the Windows CI Lua-spec job is green on the validated surface;
the new-file/rm cases use forward-slash paths and keep running. Inert on Unix
(verified: macOS still runs and passes it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove HANDOFF-windows-46.md: it was a cross-machine coordination
  scratchpad, not product docs. Durable decisions live in CONTEXT.md and
  ADR-0007; the Windows gotchas live as comments in the .ps1 shims.
- Revert the temporary feat/windows-46 push trigger now that the PR's
  pull_request trigger exercises the windows-test job. workflow_dispatch
  stays (useful for on-demand branch runs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-machine coordination doc; not product documentation. Durable
decisions live in CONTEXT.md and ADR-0007, and the Windows gotchas are
captured as comments in the .ps1 shims.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Claude Code section checked the .sh shims for an executable bit, which on
Windows means wrong artifact (the .ps1 are active) and ~6 spurious "not
executable, run chmod +x" warnings (no exec bit on Windows; chmod is a no-op).

- Add an OS-aware check_script helper: readability-only on Windows (the hook
  command invokes the interpreter explicitly), readability + exec bit on Unix.
- Check the per-OS shim extension (.ps1 on Windows, .sh on Unix) for the
  Claude Code adapter scripts and the shared nvim-socket/nvim-call shims.
- Gate the Codex/Copilot adapter-script checks behind not-Windows; on Windows
  emit one honest "not yet supported on Windows (#46)" line instead.

Unix output unchanged (verified via headless checkhealth on macOS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant