Skip to content

feat(windows): support native Windows via a per-pid named pipe - #21

Merged
Mearman merged 11 commits into
mainfrom
feat/native-windows-support
Sep 13, 2026
Merged

Mearman merged 11 commits into
mainfrom
feat/native-windows-support

Conversation

@Mearman

@Mearman Mearman commented Sep 13, 2026

Copy link
Copy Markdown
Member

Summary

Claude Code does support native Windows — it uses a per-session named pipe there rather than a Unix domain socket, confirmed directly against the official docs, with one real protocol difference: the auth line is required on native Windows (optional on macOS/Linux). This was previously misdiagnosed as a platform we should exclude entirely (a prior PR dropped Windows from the SEA matrix after the binary crashed on startup); the real fix is implementing the actual transport difference, not excluding the platform.

  • `socketPathForPid` branches on `process.platform`, producing `\.\pipe\cc-peer-` with no directory of its own (an explicit `socketDir` override still always wins, on every platform — unchanged precedence). `start()` skips its `mkdir` call on Windows accordingly.
  • `WinProcInfo` drives PowerShell's `Get-Process` for `lstart()` (Windows has no `ps`), sharing the existing signal-0 `alive()` probe and per-pid cache/in-flight-dedup logic with `PsProcInfo` via a new shared `CachedPidCommand`.
  • `handleConnection` now closes any connection on Windows whose first line isn't a valid, matching auth line, matching the documented platform difference.
  • Fixes a real, unrelated bug this surfaced: `pidDomain` was hardcoded to `"darwin"` regardless of the actual runtime platform.
  • Adds `test/windows-integration.test.ts`: real, unmocked coverage that only runs on an actual `win32` process (everywhere else it's a no-op skip), plus a new `verify-windows` CI job (windows-latest, windows-11-arm) that runs on every push/PR so a Windows regression fails the same gate as any other.
  • Re-adds Windows to the SEA release matrix now that the runtime genuinely works there.

Test plan

  • Full suite green, 100% coverage (Windows branches covered via `process.platform` mocking where a real kernel isn't available locally)
  • CI verify passes
  • verify-windows passes on both windows-latest and windows-11-arm, exercising the real named-pipe integration test
  • The two windows sea legs build, smoke, and upload a working binary

Node's net module has no real AF_UNIX support on Windows: the local
domain there is a named pipe, which must live under \\.\pipe\, never
an arbitrary filesystem path (nodejs/node#55979). Claude Code's own
official docs confirm it uses exactly this on native Windows,
alongside one real protocol difference from macOS and Linux: the auth
line is required there, not optional, and a connection whose first
line isn't a valid matching auth line is closed without delivering
anything.

socketPathForPid now branches on process.platform (an explicit
socketDir still always wins on every platform, unchanged precedence),
producing \\.\pipe\cc-peer-<pid> with no directory of its own to
create - start() skips its mkdir call on Windows accordingly.
pidFromSocketPath detects a pipe path by its own shape rather than the
current platform, since parsing should work on whichever kind of path
it is actually given.

Windows has no `ps`, so process-start verification needed its own
adapter: WinProcInfo drives PowerShell's Get-Process for lstart(),
sharing the existing signal-0 alive() probe and per-pid
cache/in-flight-dedup logic with PsProcInfo (both now built on a
shared CachedPidCommand rather than duplicating that machinery). The
exact lstart string format is a cc-peer convention, not a reproduction
of a real Windows Claude Code session's own format, which is not
publicly documented - self-consistent for cc-peer's own entries, which
is what roster admission actually needs for a peer this SDK created.

Also fixes a real, unrelated bug this work surfaced: pidDomain was
hardcoded to "darwin" regardless of the actual runtime platform.
Every other Windows-specific test in this package mocks
process.platform on a POSIX runner, which proves the branch logic but
cannot prove the OS actually accepts a named-pipe path from Node's net
module the way those tests assume. This file skips everywhere except a
genuine win32 process (describe.skipIf), so it exercises the real
default socket path, the real PowerShell-backed proc-info adapter, and
the real required-auth-line enforcement only when actually running on
Windows.
…indows job

Windows was excluded from the SEA release matrix because the runtime
crashed on startup - not a CI misconfiguration but a genuine Node
limitation (no filesystem-path AF_UNIX support on Windows) that the
prior implementation never accounted for. Now that CcPeer branches to
a real named pipe on native Windows, both windows-latest and
windows-11-arm legs build a working, functional binary again.

verify-windows runs on every push and PR, not gated behind release: a
Windows regression should fail the same gate any other regression
does, rather than surfacing only after a release already shipped it.
It runs the plain test suite (not the coverage/mutation gate, which
already runs once on Linux in verify) specifically to exercise
test/windows-integration.test.ts's real, unmocked named-pipe coverage
under an actual Windows kernel.
The rebase onto origin/main (which had removed Windows from the sea
matrix in an earlier, since-superseded commit) dropped the re-addition
this branch's own verify-windows commit assumed was already present -
restoring it explicitly here. Also rewrites the readme's "no native
Windows support" limitation, which the rest of this branch's work makes
false: it now describes the real, still-true constraints (named pipe
instead of a socket, a required auth line, an unverified procStart
format) rather than a blanket unsupported claim.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-13T06:57:23.290827Z 85c28fb PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…lesystem path

An explicit socketDir was made to win on every platform, including
Windows, where socketPathForPid then returned a literal filesystem
path such as "/tmp/xyz/1234.sock". Node has no filesystem-path AF_UNIX
support on Windows at all, so net.Server.listen() rejected every one
of these with EACCES regardless of which directory was named,
breaking the bulk of the test suite (and any real caller supplying a
socketDir) under a genuine Windows kernel.

socketPathForPid now always returns a named-pipe path on Windows, and
folds a caller-supplied socketDir into the pipe's own name (a short
hash) rather than treating it as a real directory, since a pipe has no
directory of its own to keep separate callers apart. pidFromSocketPath
parses the pid from the final "-"-delimited segment so it round-trips
either shape.

Also fixes a WinProcInfo unit test that assumed powershell.exe is
never on PATH: true on every POSIX runner this suite otherwise runs
on, but false on the real Windows CI runners this branch is meant to
support, where PowerShell is genuinely resolvable regardless of PATH
contents.
Two file-transfer tests assumed POSIX permission-bit semantics that
NTFS doesn't have: a written file's mode reads back as an actual
owner-only 0600 (NTFS has no such bit; writeFile's mode option only
ever toggles the read-only attribute), and chmod(0o000) makes a file
genuinely unreadable (on NTFS it does not, so the file the code under
test opens stays readable and the expiry branch never triggers).

Both are guarded with test.skipIf(win32); the file-transfer code
itself needs no change; POSIX runners still exercise both cases in
full.
…e-transfer validation

materialiseAttachment checked "starts with /" to decide whether a
descriptor's path was absolute before trusting it. A path staged on
Windows is drive-letter-rooted (C:\...), so every real attachment
received a "invalid transfer path" refusal there regardless of
validity. Switched to node:path's isAbsolute, which the reference
receiver's own contract ("an absolute path") actually means.
Several test fixtures assumed things that don't hold on a real
Windows kernel:

- Raw net/UdsTransport fixtures built literal filesystem paths (e.g.
  join(home, "lifecycle.sock")) directly rather than going through
  socketPathForPid, so they tried to bind a POSIX-shaped path Windows
  cannot use at all. A new shared testSocketPath helper builds a
  platform-appropriate path the same way socketPathForPid does,
  reusing the pid-agnostic fixtures needed here.
- PsProcInfo's own describe block spawns a real ps and assumes POSIX
  signal-0/init-pid semantics; it never runs on Windows in production
  (CcPeer.create() only selects it when !isWindows()), so it is now
  skipped there — WinProcInfo already has its own coverage.
- The foreign-token-tolerance and create()-selects-WinProcInfo tests
  encode POSIX-only protocol behaviour (Windows requires a valid auth
  line; WinProcInfo's own powershell.exe genuinely exists on a real
  Windows runner), both already covered by dedicated Windows-specific
  or real-Windows-integration tests elsewhere.
- A key-file permissions test relied on chmod(0o000) removing read
  access, which NTFS does not do.
Adds --fix to the cached eslint invocation so a plain pnpm lint
repairs auto-fixable violations instead of only reporting them,
matching how lint-staged already runs eslint --fix on commit.
A real, non-DI CcPeer.create() spawns a genuine child process to read
its own start time; unlike the dependency-injected fixtures elsewhere,
there is no fake procInfo to short-circuit that cost. windows-11-arm
CI runners were observed exceeding vitest's 5000ms default here, most
plausibly from an x64-under-emulation PowerShell cold start.

Introduces a shared, named timeout constant and applies it (per test
or, where every test in a suite needs it, at the describe level) to
every test that exercises a real CcPeer.create(), replacing the
existing ad hoc 10_000/15_000/20_000 literals with one consistent
value.
…volume

Creating, timing, stating and removing on the order of 200 individual
files sequentially issues far more filesystem syscalls than vitest's
5000ms default timeout was ever sized for. A windows-latest CI run
exceeded it here even though the identical code path stayed well
within it on the same run's ubuntu and windows-11-arm legs, most
plausibly NTFS latency under CI disk contention rather than anything
platform-specific to fix in the code itself.
@Mearman
Mearman merged commit 193e18f into main Sep 13, 2026
6 checks passed
@Mearman
Mearman deleted the feat/native-windows-support branch September 13, 2026 08:24
@exaclaude

exaclaude Bot commented Sep 13, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant