fix(agent): Windows pre-flight silently no-ops - cmd.exe shim truncates the multi-line prompt at the first newline #560

Closed
opened 2026-07-03 08:19:08 +00:00 by coilyco-ops · 2 comments
Member

Symptom

On the Windows host, ward agent <mode> <ref> pre-flight always returns an unusable verdict:

preflight result ... verdict=0 repo="" reason=""
preflight verdict unclear ...; proceeding open

The pre-flight agent's capture shows it received none of the issue context - it reports "no #N in the prompt", "no issue and no repository attached", runs in the empty ward-preflight-* temp dir, and returns a generic "environment healthy" note with no GO/NO-GO line. ward reads that as verdictUnknown and fails open. The gate is a silent no-op on Windows. (The detached container run is unaffected - it gets the full seeded prompt over native Linux argv - so work still lands; only the feasibility gate is dead, so NO-GO / WRONG-REPO detection never fires on Windows.)

Root cause

preflightPrompt() builds a correct, large, multi-line prompt. claude.PreflightArgv appends it as a single argv element (record.Argv.Preflight = ["claude", "-p"] -> ["claude", "-p", <prompt>]). cli-guard's shell.Runner.Capture runs it via exec.LookPath("claude") + exec.CommandContext.

On this Windows host exec.LookPath("claude") resolves to the npm batch shim C:\Users\firem\AppData\Roaming\npm\claude.cmd, because the npm dir precedes .local\bin on PATH and .CMD is in PATHEXT while .PS1 is not (so both the native claude.exe and the .ps1 are shadowed). Go's os/exec runs a .cmd through cmd.exe, and cmd.exe truncates the argument at the first newline - the newline terminates the command line.

Net: claude.cmd receives only the first line of the prompt (the generic "you are about to be sent into a container..." framing), never the Issue: ...#N, the body, or the GO/NO-GO instruction. So the agent literally cannot render a verdict.

Repro (Go, mirrors the exec path)

exec.Command("<abs>\echoargs.cmd", "-p", "line1\nline2\nline3#450") -> the shim's %2 arrives as only "line1 (leading quote, rest of the arg dropped at the first \n). Verified on this host today. A native .exe target does not truncate (CreateProcess passes the full multi-line arg up to ~32KB).

Impact

  • Windows-only. Kai's Mac host runs a native claude binary, so pre-flight works there - which is why it looked fine until now.
  • Fail-open, so no dispatched work is lost, but the pre-flight feasibility / WRONG-REPO / NO-GO gate provides zero protection on Windows.

Feed the pre-flight prompt over stdin instead of as an argv element (claude -p reads the prompt from stdin when none is given as an arg). That sidesteps Windows arg quoting / length / newline truncation on every platform and every harness, and is the most robust cross-platform fix. It touches the PreflightArgv contract (drop the appended prompt) + capturePreflight (set the capture's stdin to the prompt).

Cheaper interim options if stdin plumbing is too big for now:

  • Resolve claude to the native .exe, never the .cmd/.ps1 shim, on Windows (fragile - depends on the native exe existing and winning PATH).
  • Skip the host one-shot pre-flight on Windows and log it honestly (removes the gate rather than fixing it).

Diagnosed from the interactive host session on tower (Windows), 2026-07-03, while dispatching coilyco-flight-deck/infrastructure#450. Related host-one-shot pre-flight code: cmd/ward/agent.go (runPreflight, capturePreflight), internal/agents/claude/claude.go (PreflightArgv).

## Symptom On the Windows host, `ward agent <mode> <ref>` pre-flight always returns an unusable verdict: ``` preflight result ... verdict=0 repo="" reason="" preflight verdict unclear ...; proceeding open ``` The pre-flight agent's capture shows it received **none** of the issue context - it reports "no #N in the prompt", "no issue and no repository attached", runs in the empty `ward-preflight-*` temp dir, and returns a generic "environment healthy" note with no GO/NO-GO line. ward reads that as `verdictUnknown` and fails open. The gate is a **silent no-op on Windows**. (The detached container run is unaffected - it gets the full seeded prompt over native Linux argv - so work still lands; only the feasibility gate is dead, so NO-GO / WRONG-REPO detection never fires on Windows.) ## Root cause `preflightPrompt()` builds a correct, large, multi-line prompt. `claude.PreflightArgv` appends it as a single argv element (`record.Argv.Preflight = ["claude", "-p"]` -> `["claude", "-p", <prompt>]`). cli-guard's `shell.Runner.Capture` runs it via `exec.LookPath("claude")` + `exec.CommandContext`. On this Windows host `exec.LookPath("claude")` resolves to the **npm batch shim** `C:\Users\firem\AppData\Roaming\npm\claude.cmd`, because the npm dir precedes `.local\bin` on PATH and `.CMD` is in PATHEXT while `.PS1` is not (so both the native `claude.exe` and the `.ps1` are shadowed). Go's `os/exec` runs a `.cmd` through cmd.exe, and **cmd.exe truncates the argument at the first newline** - the newline terminates the command line. Net: `claude.cmd` receives only the first line of the prompt (the generic "you are about to be sent into a container..." framing), never the `Issue: ...#N`, the body, or the GO/NO-GO instruction. So the agent literally cannot render a verdict. ## Repro (Go, mirrors the exec path) `exec.Command("<abs>\echoargs.cmd", "-p", "line1\nline2\nline3#450")` -> the shim's `%2` arrives as only `"line1` (leading quote, rest of the arg dropped at the first `\n`). Verified on this host today. A native `.exe` target does not truncate (CreateProcess passes the full multi-line arg up to ~32KB). ## Impact - Windows-only. Kai's Mac host runs a native `claude` binary, so pre-flight works there - which is why it looked fine until now. - Fail-open, so no dispatched work is lost, but the pre-flight feasibility / WRONG-REPO / NO-GO gate provides **zero** protection on Windows. ## Recommended fix Feed the pre-flight prompt over **stdin** instead of as an argv element (`claude -p` reads the prompt from stdin when none is given as an arg). That sidesteps Windows arg quoting / length / newline truncation on every platform and every harness, and is the most robust cross-platform fix. It touches the `PreflightArgv` contract (drop the appended prompt) + `capturePreflight` (set the capture's stdin to the prompt). Cheaper interim options if stdin plumbing is too big for now: - Resolve `claude` to the native `.exe`, never the `.cmd`/`.ps1` shim, on Windows (fragile - depends on the native exe existing and winning PATH). - Skip the host one-shot pre-flight on Windows and log it honestly (removes the gate rather than fixing it). Diagnosed from the interactive host session on tower (Windows), 2026-07-03, while dispatching `coilyco-flight-deck/infrastructure#450`. Related host-one-shot pre-flight code: `cmd/ward/agent.go` (`runPreflight`, `capturePreflight`), `internal/agents/claude/claude.go` (`PreflightArgv`).
Author
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-ward-560 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-03T08:22:28Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-ward-560` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-03T08:22:28Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Owner

WARD-OUTCOME: done - regression test + doc pinning the pre-flight prompt onto stdin (the #560 Windows fix, already landed under #548) merged to main.

Honest retro: the surprise here was that the fix was already in. Reading the code before planning paid off immediately - hostOneShot already lifts every host one-shot's prompt onto the child's stdin, and #548 landed exactly the stdin change this issue recommends, with the shared chokepoint covering the pre-flight path too. So #560 turned out to be an independent same-day diagnosis of a defect that was fixed hours earlier. Rather than land an empty "already fixed" commit, I closed the real remaining gap: the #548 tests exercise a hand-shaped research prompt, so nothing pinned the pre-flight's own scenario. TestPreflightPromptRefSurvivesOntoStdin now runs the real preflightPrompt (whose Issue ref and GO/NO-GO line genuinely sit below the first newline) through the chokepoint and fails precondition-first if a future reshape ever pulls the ref up to line one. Also documented stdin as the cross-platform safety mechanism in agent-preflight.md, which only mentioned claude -p.

Confidence is high on the invariant - it's a pure-function assertion with no I/O flakiness. The one thing I could not exercise from this Linux container is the actual cmd.exe truncation on a Windows host; that verification still rests on #548's original repro. One rough edge worth a follow-up: exec.LookPath("claude") still resolves to the .cmd shim on Windows; stdin makes that harmless for the prompt, but if any future host one-shot ever needs to pass an argument on the command line, the shim-resolution hazard is still there and undocumented.

WARD-OUTCOME: done - regression test + doc pinning the pre-flight prompt onto stdin (the #560 Windows fix, already landed under #548) merged to main. Honest retro: the surprise here was that the fix was already in. Reading the code before planning paid off immediately - `hostOneShot` already lifts every host one-shot's prompt onto the child's stdin, and #548 landed exactly the stdin change this issue recommends, with the shared chokepoint covering the pre-flight path too. So #560 turned out to be an independent same-day diagnosis of a defect that was fixed hours earlier. Rather than land an empty "already fixed" commit, I closed the real remaining gap: the #548 tests exercise a hand-shaped research prompt, so nothing pinned the pre-flight's *own* scenario. TestPreflightPromptRefSurvivesOntoStdin now runs the real `preflightPrompt` (whose Issue ref and GO/NO-GO line genuinely sit below the first newline) through the chokepoint and fails precondition-first if a future reshape ever pulls the ref up to line one. Also documented stdin as the cross-platform safety mechanism in agent-preflight.md, which only mentioned `claude -p`. Confidence is high on the invariant - it's a pure-function assertion with no I/O flakiness. The one thing I could not exercise from this Linux container is the actual cmd.exe truncation on a Windows host; that verification still rests on #548's original repro. One rough edge worth a follow-up: `exec.LookPath("claude")` still resolves to the `.cmd` shim on Windows; stdin makes that harmless for the prompt, but if any future host one-shot ever needs to pass an argument on the command line, the shim-resolution hazard is still there and undocumented.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#560
No description provided.