Repository navigation
fix(agents): Windows fixes for Phase AI harness integrations - #326
Conversation
On Windows, env.sh guarded proxy routing with `kill -0 <daemon PID>`, but Claude Code's shell there is Git Bash (MSYS2), whose kill cannot see native Windows PIDs. The guard never passed: shells got the decoys but never HTTPS_PROXY, PHASE_AI_ENABLED or the CA variables, so Phase-managed commands sent decoys straight to providers while `phase ai status` reported routing as active. On Windows the guard now checks that the proxy still accepts connections (`true </dev/tcp/127.0.0.1/<port>`, a builtin with no subshell, about 3 ms); other platforms keep the PID check. Hooks still retire routing for a dead daemon through the Win32 liveness check in reconcileClaudeEnvironment. A new test sources the Windows rendering in bash (Git Bash on Windows) against a real listener, and the hook tests now assert the platform's guard instead of the literal `kill -0`.
On Windows the plugin went to os.UserConfigDir() (%AppData%\opencode\plugins), but OpenCode resolves its global config directory with xdg-basedir, which is ~/.config/opencode on every OS. OpenCode never loaded the plugin, so its shells got no Phase environment while the skill, already installed under ~/.config/opencode, said the environment was automatic. The plugin now always goes to ~/.config/opencode/plugins, beside the skill. enable and disable remove a Phase-owned copy left at the old %AppData% path; foreign files are left alone. The OpenCode tests set USERPROFILE as well as HOME, since os.UserHomeDir reads USERPROFILE on Windows.
…ifiers ApplyProcessEnvironment dropped every inherited variable whose name was not a POSIX shell identifier. Its result is an exec environment, not shell source, so this only removed real variables: on Windows every managed child lost ProgramFiles(x86) and CommonProgramFiles(x86), which vswhere-based Visual Studio discovery (node-gyp, setuptools, MSBuild) needs, and on Unix exported bash functions disappeared. Inherited names now pass through unless they are empty or contain '=' or NUL, so Windows' hidden per-drive "=C:" entries are still dropped. Keys Phase sets or unsets keep the shell-identifier check, since they also appear in sourced shell files.
The Devin tests set only HOME, but os.UserHomeDir reads USERPROFILE on Windows, so running them there installed the shared hook into the real %USERPROFILE%\.phase\ai\bin, replacing phase-hook-v1.exe with a test file.
Phase control calls from the guarded harnesses never reached the proxy on Windows: - The guard rewrote a phase.invalid call to `"C:\...\phase-hook-v1.exe" "ai" "__harness-exec" ...`. PowerShell, which Codex (including the desktop app's bundled pwsh) and Cursor use on Windows, reads a leading quoted string as an expression, so the rewritten call failed to parse. The wrapper is now rendered as an unquoted forward-slash path followed by plain words, which PowerShell 5.1 and 7, cmd and Git Bash all run. A path with spaces uses its 8.3 short name, and failing that PowerShell's call operator. - The wrapper re-runs the call in the default shell, Windows PowerShell 5.1 when pwsh is not on PATH, where curl and wget are aliases of Invoke-WebRequest. That cmdlet ignores HTTP(S)_PROXY, so the request went to DNS and phase.invalid failed to resolve. The wrapper now removes those aliases first so the real executables run. - Invoke-WebRequest and Invoke-RestMethod (iwr, irm) calls to phase.invalid were not recognized at all and failed the same way. The Codex, Cursor, Devin and OpenCode guards now reject them with a message pointing the Agent at curl, which they route. Tests run the rendered wrapper form in cmd, PowerShell and Git Bash from a plain path and from a path with a space, check that curl is the real executable inside the wrapped PowerShell, and cover the new guard denial.
Codex invoked the Phase hooks on Windows, but none of them ran. Their commandWindows entries were written as `"C:\...\phase-hook-v1.exe" "ai" "__codex-event" "guard"`, and Codex runs them in PowerShell (the desktop app's bundled pwsh 7), which reads a leading quoted string as an expression and stops with a ParserError. Codex lets a tool call through when its hook fails, so SessionStart never started the proxy, the guard never routed anything, and curl to phase.invalid went to DNS and failed to resolve. Cursor and Devin hook commands used the same quoted form, which fails the same way wherever they run in PowerShell. The renderer the previous commit added for the guard's rewritten call now lives in pkg/ai as WindowsShellCommand, and every Windows hook command uses it: Codex commandWindows, the Cursor and Devin hooks, and the harness-exec wrapper. It writes an unquoted forward-slash path followed by plain words, which PowerShell 5.1 and 7, cmd and Git Bash all run, and falls back to the 8.3 short name and then PowerShell's call operator for paths with spaces. Devin install and uninstall still recognize hook entries written in the old quoted form. The hook definitions change, so Codex asks to review them again after `phase ai enable`.
phase ai enable refreshes the adapter of every configured harness, not only the selected one. Once Codex had been enabled, every later enable, for example `phase ai enable --harness cursor`, failed with "Codex CLI is required to refresh the configured Phase plugin" wherever codex was not on PATH, for example on a Windows machine with only the Codex desktop app installed. The failure came after the skills were installed and the runtime stopped, but before the configuration was saved, so the new harness was left half set up. When the Codex CLI is missing and Codex is not the selected harness, its adapter is now left as it was, still running the shared helper that the same pass refreshes, and the summary says Codex was not refreshed and how to refresh it. Enabling Codex itself still requires the CLI, and its error now says the CLI has to be on PATH.
Brings in the 11 commits on feat--agents since aaae4de. Where both lines of work fixed the same thing, the merge keeps the feat--agents version: - The guard's wrapper command keeps feat--agents' per-harness quoting (harnessWrapperCommandLine: PowerShell's call operator for Codex, and Cursor's terminal profile choosing Cursor's form). The hook commands keep ai.WindowsShellCommand from this branch. - The OpenCode plugin path, its doc comment and its test come from feat--agents; this branch keeps only the removal of the copy earlier builds left in %AppData%. - ai_shell_scope_test.go keeps the new tests from both sides. Two pieces each side's edits removed are restored because the merged code uses them: quoteWindowsCommand, which this branch had deleted, and the runtime import in opencode_plugin.go, which feat--agents had dropped.
harnessWrapperCommandLine gives Codex, and Cursor on a PowerShell profile, PowerShell's call-operator form. Every other harness on Windows (Devin, OpenCode, and Cursor on a Git Bash or Command Prompt profile) got the double-quoted form `"C:\...\phase-hook-v1.exe" "ai" ...`. cmd and Git Bash run it, but PowerShell reads a leading quoted string as an expression and rejects it, which is what kept Codex's hooks from running. Nothing guarantees that Devin and OpenCode use cmd or Git Bash on Windows. Those harnesses now get the unquoted forward-slash form that PowerShell, cmd and Git Bash all run (ai.WindowsPlainCommand, split out of WindowsShellCommand), and the double-quoted form only when the path cannot be written unquoted even with its 8.3 name.
The guard's check for PowerShell web cmdlets accepted a cmdlet only after whitespace or a shell operator, so `pwsh -Command "iwr http://phase.invalid/..."` slipped past it: the Agent got a failed host lookup instead of the message pointing it at curl. A quote now also counts as a word edge.
…r harness phase ai enable refreshes every configured harness, so enabling Cursor or OpenCode on a machine where Codex was enabled earlier still went through Codex. With the Codex CLI on PATH it reactivated an unchanged plugin and asked to re-trust its hooks; without it, it warned that Codex was not refreshed. When another harness is selected, enable now compares the Codex plugin files with what this build would write (ai.CodexBootstrapPluginCurrent, which needs no Codex CLI) and leaves an up-to-date adapter alone, with no CLI call and no Codex notes in the summary. An outdated adapter is still refreshed when the CLI is available, and is otherwise reported as out of date.
- One PowerShell quoting helper (ai.PowerShellCallCommand) serves both the guard's wrapper and the hook fallback, so the fallback now doubles typographic single quotes too; Devin recognizes that spelling on reinstall. - Drop the %AppData% OpenCode plugin cleanup: no released build installed the plugin there. - CodexBootstrapPluginCurrent compares the plugin version and hooks without reshaping InstallCodexBootstrapPlugin. - Shorter comments.
Deploying phase-cli-install-script with
|
| Latest commit: |
ccd687b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://68c88b64.phase-cli-install-script.pages.dev |
| Branch Preview URL: | https://fix--windows-harness-env.phase-cli-install-script.pages.dev |
nimish-ks
left a comment
There was a problem hiding this comment.
Tested on windows-latest end to end: real phase ai enable for Codex (CLI 0.164.0), Cursor, Devin and OpenCode from a plain home, a home with a space (8.3 names) and a D: home without 8.3 names. Every Codex, Cursor and Devin hook ran in cmd, pwsh 7, Windows PowerShell 5.1 and Git Bash, and the guards returned their JSON decisions. The old quoted form still fails in both PowerShells. The alias removal makes curl the real executable in 5.1, and a current Codex adapter is left alone with or without the CLI on PATH.
Pushed ccd687b: one PowerShell quoting helper, so the hook fallback doubles typographic quotes too; dropped the %AppData% OpenCode cleanup, since no release installed there; a smaller Codex up-to-date check; shorter comments.
Follow-ups, not blockers:
- On a volume without 8.3 names, a path with a space gives Devin and OpenCode the cmd form of the wrapper, which PowerShell rejects. Devin's hooks fall back to the PowerShell form instead.
- In Git Bash on Windows, probing a closed port takes about 2 s, so a dead daemon slows each Claude command until the next prompt retires routing.
Summary
Fixes found while testing Phase AI on Windows 11 with Claude Code, Codex desktop app, Cursor, Open Code and Devin. Most of them kept a harness's commands away from the Phase proxy, so Agent commands ran with decoy credentials and
phase.invalidfailed to resolve; one brokephase ai enable. The branch includes a merge of the latestfeat--agents; where both sides fixed the same thing, thefeat--agentsversion was kept.Problems and fixes
Claude Code shells skipped proxy routing
env.shexportedHTTPS_PROXY,PHASE_AI_ENABLEDand the CA variables only insideif kill -0 <daemon pid>. Claude Code's shell on Windows is Git Bash, whosekillcannot see native Windows PIDs, so the routing block never ran while the decoy credentials were still exported:awssent the decoys straight to AWS, andphase ai statusstill reported routing as active.On Windows the guard now checks that the proxy port accepts connections (
true 2>/dev/null </dev/tcp/127.0.0.1/<port>), with the port taken from the proxy URL.Codex, Cursor and Devin hooks never ran
Hook commands were written as
"C:\...\phase-hook-v1.exe" "ai" "__codex-event" "guard". Codex runs them in PowerShell (the desktop app's bundled pwsh 7), which reads a leading quoted string as an expression and stops with a ParserError. Codex lets a tool call through when its hook fails, so the session never started the proxy, the guard never routed anything, andcurl.exe http://phase.invalid/v1/statusfailed with "Could not resolve host". The Cursor and Devin hook commands used the same form.Hook commands are now rendered by
ai.WindowsShellCommandas an unquoted forward-slash path followed by plain words, which PowerShell 5.1 and 7, cmd and Git Bash all run. A path with spaces uses its 8.3 short name, and failing that PowerShell's call operator. Devin install and uninstall still recognize hook entries written in the old form.The guard's rewritten command could not run in PowerShell
Devin, OpenCode, and Cursor on a Git Bash or Command Prompt profile got the rewritten Phase call in the double-quoted form, which fails the same way when the harness runs it in PowerShell. They now get the unquoted form whenever the path allows it. Codex, and Cursor on a PowerShell profile, keep the call-operator form from
feat--agents.curl and wget in Windows PowerShell missed the proxy
The wrapper re-runs a Phase call in the default shell, which is Windows PowerShell 5.1 when pwsh is not on PATH. There
curlandwgetare aliases ofInvoke-WebRequest, which ignoresHTTP(S)_PROXY, so the request went to DNS. The wrapper now removes those aliases first, so the real executables run.Invoke-WebRequest and Invoke-RestMethod calls to phase.invalid could only fail
These cmdlets, and their
iwrandirmaliases, ignore the proxy, so a call tophase.invalidcould only fail to resolve. The Codex, Cursor, Devin and OpenCode guards now reject them, including inside a quotedpwsh -Command "..."script, with a message pointing the Agent at curl.phase ai enablefailed for any harness when the Codex CLI was missingEnable refreshes the adapter of every configured harness. Once Codex had been enabled, every later enable, for example
phase ai enable --harness cursor, failed with "Codex CLI is required to refresh the configured Phase plugin" wherevercodexwas not on PATH, as on a machine with only the Codex desktop app. The failure came after the skills were installed and the runtime stopped, but before the configuration was saved, leaving the new harness half set up.When another harness is being enabled, a Codex adapter that is already up to date is now left alone. Enable compares its plugin files with what this build would write, which needs no Codex CLI, so there is no CLI call and no Codex note in the summary. An outdated adapter is still refreshed when the CLI is on PATH, and is otherwise reported as out of date in the closing summary. Enabling Codex itself still requires the CLI, and its error now says the CLI has to be on PATH.
Managed processes lost Windows environment variables
ApplyProcessEnvironmentdropped every inherited variable whose name was not a POSIX shell identifier, so every managed child on Windows lostProgramFiles(x86)andCommonProgramFiles(x86), which vswhere-based Visual Studio discovery (node-gyp, setuptools, MSBuild) needs; on Unix, exported bash functions disappeared. Inherited names now pass through unless they are empty or contain=or NUL, so Windows' hidden=C:entries are still dropped.Smaller changes
USERPROFILE, so running them on Windows no longer replaces the real~/.phase/ai/bin/phase-hook-v1.exe.Upgrading
Rerun
phase ai enableto rewrite the hook commands of every configured harness; the Codex adapter is refreshed only whencodexis on PATH. Codex then asks to review the changed hooks (/hooks) again.Testing
go vet ./...for windows, linux and darwin, andgo test ./...on Windows 11. Everything passes exceptTestEnablePreservesSymlinkedSkillPath, which needs the symlink privilege.env.shguard in real Git Bash, check thatcurlinside the wrapped PowerShell is the real executable, and cover the new guard denials and how enable treats an up-to-date or outdated Codex adapter when the Codex CLI is missing.http://phase.invalid/v1/statusthrough the proxy. The Codex desktop app runs the Phase hooks and reaches the same endpoint afterphase ai enableand re-trusting the hooks (checked before thefeat--agentsmerge).