Skip to content

fix(agents): Windows fixes for Phase AI harness integrations - #326

Merged
nimish-ks merged 12 commits into
feat--agentsfrom
fix--windows-harness-env
Oct 27, 2026
Merged

nimish-ks merged 12 commits into
feat--agentsfrom
fix--windows-harness-env

Conversation

@rohan-chaturvedi

@rohan-chaturvedi rohan-chaturvedi commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

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.invalid failed to resolve; one broke phase ai enable. The branch includes a merge of the latest feat--agents; where both sides fixed the same thing, the feat--agents version was kept.

Problems and fixes

Claude Code shells skipped proxy routing

env.sh exported HTTPS_PROXY, PHASE_AI_ENABLED and the CA variables only inside if kill -0 <daemon pid>. Claude Code's shell on Windows is Git Bash, whose kill cannot see native Windows PIDs, so the routing block never ran while the decoy credentials were still exported: aws sent the decoys straight to AWS, and phase ai status still 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, and curl.exe http://phase.invalid/v1/status failed with "Could not resolve host". The Cursor and Devin hook commands used the same form.

Hook commands are now rendered by ai.WindowsShellCommand 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. 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 curl and wget are aliases of Invoke-WebRequest, which ignores HTTP(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 iwr and irm aliases, ignore the proxy, so a call to phase.invalid could only fail to resolve. The Codex, Cursor, Devin and OpenCode guards now reject them, including inside a quoted pwsh -Command "..." script, with a message pointing the Agent at curl.

phase ai enable failed for any harness when the Codex CLI was missing

Enable 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" wherever codex was 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

ApplyProcessEnvironment dropped every inherited variable whose name was not a POSIX shell identifier, so every managed child on Windows lost ProgramFiles(x86) and CommonProgramFiles(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

  • The Devin hook tests set USERPROFILE, so running them on Windows no longer replaces the real ~/.phase/ai/bin/phase-hook-v1.exe.

Upgrading

Rerun phase ai enable to rewrite the hook commands of every configured harness; the Codex adapter is refreshed only when codex is on PATH. Codex then asks to review the changed hooks (/hooks) again.

Testing

  • go vet ./... for windows, linux and darwin, and go test ./... on Windows 11. Everything passes except TestEnablePreservesSymlinkedSkillPath, which needs the symlink privilege.
  • New tests run the rendered commands in cmd, Windows PowerShell, pwsh and Git Bash from a plain path and a path with a space, source the Windows env.sh guard in real Git Bash, check that curl inside 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.
  • Manual, on Windows 11: Claude Code shells reach http://phase.invalid/v1/status through the proxy. The Codex desktop app runs the Phase hooks and reaches the same endpoint after phase ai enable and re-trusting the hooks (checked before the feat--agents merge).

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.
@rohan-chaturvedi
rohan-chaturvedi marked this pull request as ready for review October 27, 2026 07:37
- 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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Deploying phase-cli-install-script with  Cloudflare Pages  Cloudflare Pages

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

View logs

@nimish-ks nimish-ks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@nimish-ks
nimish-ks merged commit c835d87 into feat--agents Oct 27, 2026
13 checks passed
@nimish-ks
nimish-ks deleted the fix--windows-harness-env branch October 27, 2026 08:13
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.

2 participants