Text that leaves Windows through curl or Windows PowerShell can lose every character outside ASCII before the request is even built, and the server still answers with success. On our test PC, the curl that ships inside Git for Windows converted its command-line arguments to the ANSI code page: Cyrillic arrived as question marks, while dashes, curly quotes and accented letters arrived as lone bytes that a UTF-8 page renders as �. Windows PowerShell did much the same to a string body whenever -ContentType carried no charset. Do language models avoid this? They know the theory, but they often forget it while writing the actual command. Given 13 everyday requests that never mention encoding, Claude Sonnet got 9 right on its own and Claude Haiku 8; with our rules in the prompt, both got all 13.
How we ran into it
We hit it ourselves. On 24 and 25 October 2026 we sent catalog listings from Git Bash with curl, the text typed straight into the command. Every request came back as a success. Later, two directories showed � where the dashes in our product names should have been. One of them had given us an edit link, so we sent the corrected names through it, this time letting curl read the body out of a file, and compared the saved record with the original. The other had no way to edit an entry. We asked before submitting again, and the broken copies are still waiting in its review queue.
Afterwards we added a hook to our agent harness that blocks any curl command carrying inline data or a character outside ASCII, with a test that feeds it deliberately bad commands. Then we measured what actually breaks, and gave the same jobs to two models.
What reached the server
We posted one test string in every variant we could think of to a local server that stores the raw request body, then compared bytes. The string held a Bulgarian word, an em dash, low and high quotation marks, a typographic apostrophe, the euro sign and a check mark: 77 bytes in UTF-8. The test PC: Windows 11 with Windows-1252 as its ANSI code page, Node 24, Windows PowerShell 5.1 as built into the system, the curl 8.19 that Git for Windows installs, and the curl.exe 8.21 that sits in System32.
Lost on the way:
| How the text was sent | What the server received |
|---|---|
Git's curl, text in -d, --json, -F name=…, inline or through "$VAR" |
43 bytes instead of 77: Cyrillic and ✓ as ?, each of — „ “ ’ € é ü as a single byte |
Git's curl, -d "$(cat body.json)" |
the same 43 bytes, because the shell pastes the file back onto the command line |
Git's curl, --data-urlencode 'text=…' |
percent-encoded damage: %3F for each Cyrillic letter, %97 for the dash |
PowerShell, Invoke-RestMethod, body passed as a string, no charset |
Cyrillic and € as ?, — as -, „ “ as ", ’ as '; the Latin part looks fine |
PowerShell, the same with the charset given in -Headers |
the same loss |
PowerShell, a JSON string passed as an argument to curl.exe |
the inner double quotes stripped, even from plain ASCII JSON |
PowerShell, body file from Set-Content, Out-File or >, or -Encoding UTF8 |
in turn: ANSI bytes, UTF-16LE plus BOM, UTF-8 plus BOM ahead of the JSON |
PowerShell, curl -d … |
nothing at all, while the script still reported success (exit code 0) |
Arrived byte for byte:
- Git's curl opening the file by itself:
--data-binary @body.jsonfor a JSON body,-F 'title=<title.txt'for a form field,--data-urlencode 'text@text.txt'for a URL-encoded one. - A body written in Git Bash with
printfor a quoted heredoc, also when the value came from a shell variable throughprintf. - Every non-ASCII character written as a
\uescape, so that the command line itself is pure ASCII. - Node
fetchfrom a script file, and alsonode -ewith the text inline. - In PowerShell: the charset inside
-ContentType, UTF-8 bytes handed over as the body,-InFile, or a here-string holding ’ „ “ —. - The System32 curl.exe with the text inline.
The last item is the trap. The curl.exe that Windows itself provides handles its arguments as Unicode text, and it delivered every byte. So a script that is fine on your PC may break on a colleague's, and the only difference is which curl comes first in PATH. On our machine Git's folder came first, so even typing curl.exe in PowerShell ran Git's build.
Microsoft documents most of the PowerShell half. In Windows PowerShell, Set-Content falls back to the system's legacy ANSI code page for a new file, while Out-File and the redirection operators produce UTF-16LE, and every Unicode encoding except UTF-7 adds a BOM (about_Character_Encoding). The cmdlet behind the curl alias encodes the body with the charset named in -ContentType and otherwise falls back to a default (Invoke-WebRequest). Curly quotes count as ordinary quotation marks, so a typographic apostrophe closes a single-quoted string (about_Quoting_Rules). The part about Git's curl and its arguments we found only by measuring.
Method
We wrote 13 requests phrased the way people actually ask, and none of them mentions encoding. Five are for Git Bash: a JSON listing in Bulgarian, a forum post in English full of accents and curly quotes, a file upload as a multipart form, a loop that reads product names from a file, and a URL-encoded chat message. Four are for Windows PowerShell: a CRM record through Invoke-RestMethod, a curl example copied from API documentation, a body saved to a file and then sent by curl.exe, and a script for Task Scheduler. Three are diagnoses that start from a garbled public page. The thirteenth asks the model to clean up after text went out broken to four directories.
Each model answered each request once on its own and once with the text of our skill windows-utf8-request-body added to its system prompt. Both models were called from the command line in Claude Code's non-interactive mode, without tools or personal settings: 52 runs in all.
Code scored every answer, with the same checks on both sides. An answer failed if text outside ASCII or a shell variable sat in a curl data argument, if it typed the PowerShell curl alias, if it let Invoke-RestMethod encode a string while -ContentType named no charset, if it wrote the body with Set-Content, Out-File, a redirect or an encoding that adds a BOM, if it handed a JSON string to curl.exe, or if it placed a curly quote inside a PowerShell string of the kind that quote closes. To pass, the answer also had to load the text out of a file, run it from a script or pass bytes, and leave it unchanged or written as escapes.
Before the run, a control fed the checks 13 ideal answers and 8 other correct ones, which all passed, and 24 wrong ones, which all failed. After the run we read every failure by hand and relaxed one check for both sides: saving the scheduled script itself with -Encoding UTF8 is acceptable, because a script file holding Cyrillic needs exactly that BOM.
Results
| With the skill | Without | |
|---|---|---|
| Sonnet: requests handled right | 13 of 13 | 9 of 13 |
| Haiku: requests handled right | 13 of 13 | 8 of 13 |
What Sonnet knew without help. In each of the three diagnoses it placed the damage on the sending machine, before the request left, and blamed a legacy encoding rather than the server. It knew that Windows PowerShell ignores a charset passed through -Headers and that curl is an alias there. For the two JSON bodies in Git Bash it picked a safe path unprompted, a body file once and \u escapes once, and it handled the repair well.
Where it slipped, while writing commands. For the multipart upload and for the URL-encoded chat message, it put the Bulgarian text straight into -F "title=…" and --data-urlencode "text=…", and both times it reassured the user that the Git Bash terminal speaks UTF-8, so the text would arrive intact. The terminal is not where the loss happens; Git's curl loses the text while reading its own arguments. In the loop it escaped quotes in each product name with care, then passed the result to curl as --data-binary "{\"name\":\"$name\"}", which is still an argument. In the PowerShell example adapted from API documentation it sent UTF-8 bytes with the right charset, yet wrote 'Mara’s Bakery — Варна', a string that the curly apostrophe closes right after Mara.
Haiku without help failed the same four requests and one more: it sent the English post with accents, curly quotes and the euro sign inline in -d, with the advice to run it in a UTF-8 terminal. Its PowerShell mistake was a cousin of Sonnet's: it put „ “ inside double-quoted strings, which end there in the same way. Like Sonnet, it got the three diagnoses and the repair right.
With the skill, both models moved every non-ASCII body into a file that curl opened by itself or into a Node script with fetch, used here-strings with a charset in -ContentType or UTF-8 bytes in PowerShell, chose curl.exe over the alias, and asked before submitting a second time to the directory that offered no edit route.
The pattern is the interesting part. Explaining the bug and avoiding it are different skills: both models diagnosed a garbled page correctly every time, and both still produced the bug when the task was simply "give me the command". A single dash can do damage elsewhere too; one once failed our first real x402 payment for a reason that had nothing to do with Windows.
A check you can run
- See which curl you are actually calling:
where.exe curlin PowerShell,type -a curlin Git Bash. Whichever is listed first is what runs. - Send a known string with Cyrillic, an em dash and a curly apostrophe to a server you control, and compare bytes rather than looks.
- After every real write, fetch the record and check it against the original text in code. A hyphen standing where an em dash was looks perfectly normal to the eye.
- Damage of the
ékind can often be undone by code, as we saw when we pitted code against a model on repairs. A letter that turned into?cannot: the record keeps only the question mark, so the fix starts again from the source text.
What we did not measure
- One machine, one code page. Windows 11 with Windows-1252. A machine set to another code page would garble the text in other ways; we did not measure which.
- PowerShell 7 and cmd.exe. Neither was measured. Microsoft says PowerShell 7.3 changed how arguments reach native programs, a breaking change from Windows PowerShell (about_Parsing), so several rows above may not hold there.
- Tool versions. Git's curl 8.19 and the System32 curl.exe 8.21 only.
- Checks, not execution. We scored the answers with code that looks for safe and unsafe patterns. We did not run the models' commands against a server, so an answer could pass every check and still fail for another reason.
- One run, our own requests. Each model answered each request once per side, and we wrote all 13 requests, partly from the mistake we had just made ourselves. Running everything again might flip a borderline case in one direction or the other.
- One check relaxed after the run. It applied to both sides. Without it, Sonnet's score on its own would be 8 rather than 9; the other three scores stay the same.
Read this post as Markdown: /blog/windows-curl-powershell-utf8-what-models-miss.md · Atom feed.
