Skip to content

Add Windows PowerShell hook commands - #2

Open
AndreKalberer wants to merge 4 commits into
warpdotdev:mainfrom
AndreKalberer:andrekalberer/codex-hooks-powershell-windows
Open

Add Windows PowerShell hook commands#2
AndreKalberer wants to merge 4 commits into
warpdotdev:mainfrom
AndreKalberer:andrekalberer/codex-hooks-powershell-windows

Conversation

@AndreKalberer

@AndreKalberer AndreKalberer commented Jul 8, 2026

Copy link
Copy Markdown

Summary

  • Add commandWindows overrides so Codex uses PowerShell hook entrypoints on Windows instead of invoking .sh files directly.
  • Add native PowerShell implementations for the Warp notification hooks and Oz orchestration bridge hooks.
  • Document the Windows hook path and extend hook manifest tests to cover the Windows overrides.

Linked Issue

Closes warpdotdev/warp#13391

Manual Windows Evidence

Before - upstream main uses .sh hook commands on Windows

Tested against upstream main at f11334d. Installing warp@codex-warp and running the Codex hook smoke test causes Windows to show the app-selection dialog for opening a .sh file.

before-sh-dialog-compressed.mp4

After - this PR uses PowerShell hook commands on Windows

Tested against this PR at 5c01e7c. Installing warp@codex-warp from the patched branch and running the same Codex hook smoke test completes without the .sh app-selection dialog. The visible hook log shows SessionStart, UserPromptSubmit, and Stop completing.

after-powershell-hooks-compressed.mp4

Testing

  • Get-ChildItem -Recurse -Filter *.ps1 | ForEach-Object { ... [System.Management.Automation.Language.Parser]::ParseFile(...) ... }
  • Get-Content -Raw plugins/warp/hooks/hooks.json | ConvertFrom-Json; Get-Content -Raw plugins/orchestration/hooks/hooks.json | ConvertFrom-Json
  • C:\Program Files\Git\bin\bash.exe tests/test-hooks.sh
  • Windows PowerShell smoke: Warp payload builder + orchestration mailbox drain/stop block
  • Windows PowerShell smoke: orchestration listener launch through on-session-start.ps1 + cleanup through on-session-end.ps1
  • Manual Windows before/after capture using warp@codex-warp from upstream main (f11334d) and this PR branch (5c01e7c)

@AndreKalberer
AndreKalberer marked this pull request as ready for review July 8, 2026 21:18
@AndreKalberer

Copy link
Copy Markdown
Author

@liliwilson Would you be able to take a look when you have a chance? This implements the commandWindows approach from warpdotdev/warp#13391, with Windows PowerShell regression coverage. The fork workflow is currently awaiting maintainer approval.

@TheQmaks

Copy link
Copy Markdown

Hi @AndreKalberer — thanks for this PR, I've been running it on native Windows and it's very close, but I hit one blocking issue with the notification transport that I'd like to share along with a working fix.

The problem

On native Windows (Windows 11, Windows PowerShell 5.1, Codex running inside Warp's ConPTY), Send-WarpNotification never delivers anything to Warp: [System.IO.File]::Open("CONOUT$", ...) throws System.NotSupportedException — the managed FileStream path refuses console devices under ConPTY. The surrounding try/catch swallows the exception, so every event (session_start included) is silently dropped and Warp never learns the Codex session id. The hooks look installed and healthy, which makes this painful to diagnose.

Stdout/stderr can't be used as a fallback either: the Codex hook runner reserves stdout for hook-control JSON and captures stderr, so neither reaches the PTY.

The fix

Write to the console directly through the Win32 API: CreateFileW("CONOUT$") -> WriteConsoleW -> CloseHandle. Patch against plugins/warp/scripts/common.ps1:

diff --git a/plugins/warp/scripts/common.ps1 b/plugins/warp/scripts/common.ps1
index 6c91da7..5d40f26 100644
--- a/plugins/warp/scripts/common.ps1
+++ b/plugins/warp/scripts/common.ps1
@@ -123,18 +123,93 @@ function Send-WarpNotification {
     $message = "$escape]777;notify;$Title;$Body$bell"
 
     try {
-        $stream = [System.IO.File]::Open("CONOUT$", [System.IO.FileMode]::Open, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Write)
-        try {
-            $writer = New-Object System.IO.StreamWriter($stream, [Console]::OutputEncoding)
-            $writer.AutoFlush = $true
-            $writer.Write($message)
-        } finally {
-            if ($writer) {
-                $writer.Dispose()
-            } else {
-                $stream.Dispose()
+        if (-not ("WarpConsoleWriter" -as [type])) {
+            Add-Type -TypeDefinition @"
+using System;
+using System.Runtime.InteropServices;
+
+public static class WarpConsoleWriter
+{
+    private const uint GenericWrite = 0x40000000;
+    private const uint FileShareRead = 0x00000001;
+    private const uint FileShareWrite = 0x00000002;
+    private const uint OpenExisting = 3;
+
+    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    private static extern IntPtr CreateFileW(
+        string fileName,
+        uint desiredAccess,
+        uint shareMode,
+        IntPtr securityAttributes,
+        uint creationDisposition,
+        uint flagsAndAttributes,
+        IntPtr templateFile);
+
+    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    private static extern bool WriteConsoleW(
+        IntPtr consoleOutput,
+        string buffer,
+        uint charsToWrite,
+        out uint charsWritten,
+        IntPtr reserved);
+
+    [DllImport("kernel32.dll")]
+    private static extern bool CloseHandle(IntPtr handle);
+
+    public static bool Write(string message)
+    {
+        IntPtr handle = CreateFileW(
+            "CONOUT$",
+            GenericWrite,
+            FileShareRead | FileShareWrite,
+            IntPtr.Zero,
+            OpenExisting,
+            0,
+            IntPtr.Zero);
+
+        if (handle == new IntPtr(-1))
+        {
+            return false;
+        }
+
+        try
+        {
+            // WriteConsoleW counts UTF-16 chars and may write fewer than requested for
+            // large buffers, so loop until the whole message is out. Avoid splitting a
+            // surrogate pair at a chunk boundary.
+            int offset = 0;
+            while (offset < message.Length)
+            {
+                int chunkLength = Math.Min(16384, message.Length - offset);
+                if (offset + chunkLength < message.Length
+                    && char.IsHighSurrogate(message[offset + chunkLength - 1]))
+                {
+                    chunkLength--;
+                }
+
+                string chunk = message.Substring(offset, chunkLength);
+                uint written;
+                if (!WriteConsoleW(handle, chunk, (uint)chunk.Length, out written, IntPtr.Zero)
+                    || written == 0)
+                {
+                    return false;
+                }
+
+                offset += (int)written;
             }
+
+            return true;
+        }
+        finally
+        {
+            CloseHandle(handle);
         }
+    }
+}
+"@
+        }
+
+        [void][WarpConsoleWriter]::Write($message)
     } catch {
         # Hook stdout is reserved for Codex hook control JSON. If there is no
         # attached console device, drop the notification rather than emitting

Implementation notes:

  • WriteConsoleW counts UTF-16 chars (not bytes) and may write fewer than requested for large buffers, so the loop resumes from the partial-write offset and avoids splitting a surrogate pair at a chunk boundary. Large payloads are real: permission_request embeds the full tool_input, which for a Write/Edit tool is the whole file body.
  • INVALID_HANDLE_VALUE and write failures return false; the handle is always closed; nothing is emitted to stdout/stderr, so hook-control JSON stays clean.
  • Plain Windows PowerShell 5.1 compatible (no PS7-only syntax).
  • Known cost: Add-Type compiles on every hook invocation (~330 ms measured) since each hook is a fresh powershell.exe. Correctness-wise fine; could later be optimized with a precompiled assembly if it matters.

Verification (Windows 11, Windows PowerShell 5.1.26100)

  • All .ps1 files parse clean with the PS 5.1 language parser.
  • Direct transport test: small string → True; 200,000 chars → True in ~19 ms (exercises the partial-write loop); 40,000 chars of surrogate pairs → True.
  • End-to-end: with this patch, Codex session_start reliably reaches Warp over OSC 777 on ConPTY, where the current PR head delivers nothing.

Two smaller observations while testing, take or leave:

  1. on-permission-request.ps1 puts the full untruncated tool_input into the payload (only the human summary is truncated). Capping it would keep OSC sizes sane regardless of transport.
  2. Since this materially changes the Windows transport, bumping the plugin version past 0.4.0 would help installed copies pick up the fix (the plugin cache is keyed by version).

Relates to warpdotdev/warp#13391 (Codex hooks on Windows defaulting to PowerShell).

@AndreKalberer

Copy link
Copy Markdown
Author

Thanks for the detailed report and patch, @TheQmaks. I reproduced the File.Open("CONOUT$") failure on Windows PowerShell 5.1 and pushed be10d0c using CreateFileW/WriteConsoleW with partial-write handling. I also bumped both plugins to 0.4.1 and added interop coverage. Local PowerShell and Bash tests pass; the GitHub workflow is awaiting maintainer approval. I kept tool_input unchanged for now to preserve parity with the Unix hook.

@TheQmaks

Copy link
Copy Markdown

Verified be10d0c on native Windows 11 + Windows PowerShell 5.1.26100 (Codex running inside Warp's ConPTY):

  • All .ps1 files parse clean with the PS 5.1 language parser.
  • .\tests\test-hooks.ps1 passes locally with the same invocation as the new windows-plugin-tests CI job (exit 0).
  • Direct checks against the committed common.ps1: small write → True; 200,000 chars → True; 40,000 chars of surrogate pairs → True; session_start payload correctly reports 0.4.1.
  • The committed transport is code-identical to the patch I had running end-to-end here (Codex session_start reaching Warp over OSC 777), so I'm confident in the E2E behavior.

Keeping tool_input as-is for Unix parity makes sense — the partial-write loop covers the large-payload case regardless. LGTM from the Windows side, thanks for the quick turnaround!

@liliwilson
liliwilson requested a review from acarl005 July 18, 2026 00:33
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.

Codex hooks on windows should default to powershell

2 participants