PowerShell mangles quotes in CLI args, so pass JSON by file

2 min read PowerShellWindows

A curl or gh command with inline JSON works in bash and cmd but the payload arrives corrupted from PowerShell. PowerShell rewrites embedded double quotes when handing args to native executables. Pass the payload by file or stdin instead.

TL;DR · THE FIX

PowerShell rewrites embedded double quotes when passing arguments to native exes, so inline JSON arrives mangled and the server 400s. Pass the payload by file (curl -d @body.json, gh api --input body.json) or stdin. Inline escape hatches: the --% stop-parsing token, or $PSNativeCommandArgumentPassing = 'Standard' on PowerShell 7.3+.

The symptom

A request with an inline JSON body worked from bash and from cmd, then 400’d every time from PowerShell:

curl -X POST https://api.example.com/things `
  -H "Content-Type: application/json" `
  -d "{""name"":""test"",""count"":3}"

The server complained the body was malformed. The exact same JSON, pasted into a file and posted from curl on a Mac, went through fine. So it wasn’t the API and it wasn’t the JSON.

What’s actually happening

PowerShell doesn’t pass your argument string to a native executable verbatim. It re-parses it and rebuilds the command line, and its rules for embedded double quotes differ from cmd’s, so the inner quotes get dropped or doubled in transit. By the time curl.exe receives the argument, the JSON is no longer valid. Windows PowerShell 5.1 is the worst offender, but it bites in 7.x too. You are not escaping wrong; the shell is rewriting your escaping.

The fix

Stop passing complex quoted payloads inline. Put the body in a file and reference it, which sidesteps the quoting entirely:

'{"name":"test","count":3}' | Out-File body.json -Encoding utf8
curl -X POST https://api.example.com/things -H "Content-Type: application/json" -d "@body.json"

# gh has the same escape hatch:
gh api /repos/owner/repo/issues --input body.json

Piping the payload to the command’s stdin works too. If you genuinely must inline it, you have two options:

  • The stop-parsing token --% tells PowerShell to pass the rest of the line literally: curl --% -d "{\"name\":\"test\"}" .... Note it also turns off PowerShell variable expansion for the rest of the line.
  • On PowerShell 7.3+, set $PSNativeCommandArgumentPassing = 'Standard' and inline quoting behaves like you’d expect.

The lesson

When a native CLI works elsewhere but mangles its payload from PowerShell, the shell is rewriting your quotes, not the tool. Pass anything with embedded quotes by file or stdin and the problem disappears. Keep inline JSON for bash; in PowerShell, reach for -d "@file.json".

Related fixes

Discussion

Powered by GitHub. Sign in to leave a comment.