DEV Community

A Batch-File Shim Was Truncating My Agent's Prompts on Windows (3/24 to 21/24)

Environment: Windows 11 Home, Python 3.x, Claude Code installed via npm install -g @anthropic-ai/claude-code . Measured 2026-08-14 to 2026-08-16. I was comparing two agent harnesses on the same set of web tasks. The first run of harness B scored 3 out of 24. I didn't touch the model and I didn't touch the prompt wording. I changed only how the prompt reached the process, and the score went to 21/24. Here is what was actually broken. What happened I was running an eval to see how well current agents handle routine web work, the kind a triage process would flag as "candidate for RPA." Two harnesses, same 8 tasks: - Harness A: the computer use API directly (claude-sonnet-5, max_turns=40 ), 8 tasks x 1 trial. - Harness B: Claude Code headless ( claude -p ) plus Playwright, same model, 8 tasks x 3 trials = 24 runs. Success in both harnesses is decided by machine verification only: submitted form payloads compared against expected JSONL, downloaded files checked for existence and size, answers matched by regex. The agent's own claim of "done" is never trusted as a success signal. First run of harness B: 3/24. Only the first task passed. I assumed the model was the weak link and started digging into what harness B was actually sending it. The core finding: a multi-line prompt loses everything after the first newline On Windows, when you drive claude -p from Python via subprocess and let shutil.which resolve the executable, a multi-line prompt gets truncated at the first newline before the process ever sees it. Everything after line 1 is silently dropped. Root cause: the npm global install of Claude Code puts a file named claude.CMD on PATH. That's the file shutil.which("claude") resolves to - not the real executable. It's a batch wrapper. Its body is: "%dp0%\node_modules@anthropic-ai\claude-code\bin\claude.exe" %* Let me separate what I measured from what I'm inferring. What I measured: swapping only argv[0] between the two paths changes the outcome. Point it at claude.CMD and the tail is gone; point it at claude.exe and it arrives. Prompt, flags, and environment are identical, so the difference is the shim. What I'm inferring: %* is cmd.exe's mechanism for forwarding arguments to the wrapped command, and the newline appears to be lost in that forwarding. I have not isolated which layer drops it (cmd.exe's argument expansion, the shell Python interposes when executing a .CMD , or both). Either workaround below is sufficient without knowing, so I stopped there. Minimal reproduction: import os, subprocess EXE = os.path.join(os.environ["APPDATA"], "npm", "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe") CMD = os.path.join(os.environ["APPDATA"], "npm", "claude.CMD") PROMPT = "Follow the instruction below exactly.\nOutput the string MARKER_TAIL_9137 and nothing else." for label, argv0 in (("claude.CMD (shim)", CMD), ("claude.exe (direct)", EXE)): r = subprocess.run([argv0, "-p", PROMPT], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=180) out = (r.stdout or "") + (r.stderr or "") print(label, "tail_received =", "MARKER_TAIL_9137" in out) Measured result: - claude.CMD (shim) :tail_received = False . The model's reply asks for the missing instruction, having seen only line 1: "Follow the instruction below exactly." - claude.exe (direct) :tail_received = True . There are two fixes: - Resolve and call claude.exe directly instead of trusting whatevershutil.which("claude") returns. - Normalize newlines to spaces in the prompt before passing it. This is the more portable option, since it sidesteps the shim regardless of platform. This is hard to catch because there's no exception, no non-zero exit code, and no warning anywhere in the pipeline. cmd.exe forwards a truncated string and everything downstream treats it as a normal, well-formed prompt. The only symptom is a low success rate, which looks identical to "the model isn't capable enough for this task." I spent time suspecting the model before I suspected the wrapper. How harness B's score moved (measured) - 3/24: first run. Only the first task passed. The prompt never actually included the target URL - I'd assumed the model could infer it from task context, which was wrong for a browser-driving harness. Harness A never surfaced this, because in harness A I navigate the browser to the URL programmatically before the agent's turn starts, so the missing URL in the prompt didn't matter there. - 6/24: after adding the URL to the prompt. Only tasks whose instruction was a single line started passing; multi-line task instructions still failed. That asymmetry is what led to the newline finding - the agent's own reply, when it failed, said the instruction seemed cut off, and the cutoff point matched exactly where the newline sat in the task definition. - 23/24: after normalizing newlines. I discarded this number. Harness B had no --model flag set and was running on whatever the CLI's default model was, so this result wasn't comparable to harness A, which was pinned to sonnet-5. Confounded comparison, not a usable data point. - 21/24: after pinning both harnesses to claude-sonnet-5 explicitly and re-running. This is the number I'm keeping. The measurement itself - Harness A: 6/8 successes, 993.9 JPY total. Both failures were turn-limit cutoffs, not wrong answers: a 10-row transcription task finished 1 row before hitting the cap (251.7 JPY spent), and a 3-item conditional submission task finished 1 item before the cap (283.4 JPY spent). - Harness B: 21/24 successes, 0 JPY (covered under a flat-rate plan, so no marginal API cost). All three failures were over-submission - the agent submitted 11 entries where 10 were expected, and separately 4 where 3 were expected. - On harness A, what separates cheap runs from expensive ones is submission count, not task difficulty. Tasks that were read-once-answer-once finished in 5 to 7 turns at 12 to 24 JPY. Tasks requiring repeated submissions burned through all 40 turns and only completed 10 to 33 percent of the required work. - 993.9 JPY is an estimate, not an invoiced amount. It's derived from list pricing ($3 input / $15 output per Mtok) converted at 150 JPY per USD. - Caveat: the stop conditions are asymmetric. Harness A had max_turns=40 plus a 600-second timeout; harness B had only the timeout, no turn cap. Part of the 6/8 vs 21/24 gap comes from that difference, so don't read those two numbers as a head-to-head ranking. The claim of this post is the within-harness-B change, 3/24 to 21/24. Closing Success rate is the product of model capability and harness quality, and the two are easy to conflate when the harness fails silently. Before concluding a model can't do a task, I now check four things: is the target (URL, path) actually present in what gets sent; is the full instruction arriving intact (print the exact prompt the process received, and read it - don't assume); can the agent actually reach whatever files it's supposed to produce or read; are model and stop conditions pinned explicitly on both sides being compared. If I'd taken that first run at face value, I would have reported that Claude Code plus Playwright can't handle routine web work. The cause had nothing to do with the model: a batch file was dropping everything after the first newline. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.