PowerShell 7.3+ Native Argument Passing: Quotes, Empty Values, and Compatibility
Understand PowerShell native argument modes, test the exact argv received, and migrate scripts safely across Windows, macOS, and Linux.
Calling an executable from PowerShell looks shell-like, but it crosses a boundary between two different command languages. PowerShell parses expressions and strings first, then has to turn the result into arguments in the representation expected by the operating system and target program. That boundary is where empty strings, quotes, embedded whitespace, and shell metacharacters become surprising.
PowerShell 7.3 made the newer native-command argument behavior the default. This fixed long-standing cases where arguments were lost or rewritten, but it can expose scripts that were written around legacy behavior. The right migration strategy is not to add more backslashes until one command happens to work. First identify what argument sequence the child process must receive, then test that sequence on every supported PowerShell and platform combination.
Know which parser owns each character
PowerShell parses its own source before launching a native executable. A quoted PowerShell string is a value; its quotes generally delimit the string and do not become part of the value. For example, "two words" is one PowerShell string argument containing a space. The native program then receives an argument vector or a command-line representation reconstructed from those values, depending on platform and API.
Do not reason from how a command looks when echoed in a transcript. A rendered command line is not a reliable inspection of the child’s final arguments. Test with a small diagnostic executable that prints every argument with delimiters, or use an application-specific dry-run mode. A useful test matrix includes no arguments, one empty argument, whitespace-only input, a path with spaces, embedded quote characters, trailing backslashes before a quote, wildcard characters, and a string beginning with a dash.
On PowerShell 7.3 and later, $PSNativeCommandArgumentPassing selects the behavior. The valid values are Legacy, Standard, and Windows. Legacy preserves the historical PowerShell behavior. Standard uses the improved argument serialization. Windows behaves like Standard except that selected legacy Windows targets such as cmd.exe, cscript.exe, wscript.exe, find.exe, sqlcmd.exe, and scripts ending in .bat, .cmd, .js, .vbs, or .wsf are invoked using legacy rules. The platform default is Windows on Windows and Standard on non-Windows systems.
$PSNativeCommandArgumentPassing
# Scope a compatibility experiment to a block instead of changing the
# behavior for the rest of an interactive session.
& {
$PSNativeCommandArgumentPassing = 'Legacy'
# Invoke only the known compatibility-sensitive executable here.
}
The assignment above is a diagnostic technique, not a blanket fix. A script that sets Legacy globally can make arguments behave differently on another host and can conceal assumptions that should be fixed at the call site. Prefer the platform default for ordinary executables, and scope a compatibility override narrowly while documenting which target requires it.
Preserve empty and quoted arguments intentionally
The difference between no argument and an empty argument matters to native programs. A command-line option may use an empty value to mean “clear this setting,” while omission means “use the default.” Legacy argument handling could drop empty strings in cases where the newer modes preserve them. Likewise, embedded quote characters may be meaningful data, not PowerShell syntax.
$configPath = 'C:\Program Files\Contoso\settings.json'
$pattern = 'name="blue team"'
$emptyValue = ''
# Each value should be tested as one logical argument at the target process.
some-native-tool.exe --config $configPath --filter $pattern --label $emptyValue
The example demonstrates the intent, not a guarantee that every older runtime and target combination serializes every corner case identically. In particular, Windows batch files are not ordinary executables with a simple argv contract: under Windows argument passing, they are ultimately interpreted through cmd.exe. Microsoft explicitly cautions against sending untrusted data to batch files as arguments. Do not treat quoting as a security boundary. Validate and constrain data before passing it to an interpreter or script host.
For a reproducible regression test, create a tiny native helper that prints an argument count and each argument with visible delimiters or lengths. Keep the helper in a test fixture, not in production paths. Run the test under the minimum supported PowerShell version and the current supported version on Windows, macOS, and Linux. PowerShell 5.1 does not expose the 7.3 preference variable, so compatibility testing must distinguish Windows PowerShell from modern PowerShell rather than assuming the shell named powershell is a single implementation.
Treat --% as a specialized Windows escape hatch
The stop-parsing token --% changes how the rest of a command line is interpreted for a native application on Windows. It is not a general replacement for normal quoting, and it does not apply to PowerShell cmdlets as if they were external tools. It is most useful when forwarding syntax that belongs to another Windows command-line parser and would otherwise be interpreted by PowerShell.
# Illustrative Windows-only case: the remainder is passed with limited
# PowerShell interpretation to cmd.exe.
cmd.exe /c --% echo "literal text with | punctuation"
This token has tradeoffs: it limits normal variable expansion and does not create a portable argument-building API. If the command must combine dynamically generated values, do not splice untrusted data into a raw command string. Prefer an executable interface that accepts discrete arguments, a documented input file, standard input, or an API that takes a structured argument collection. Start-Process is not automatically safer if its -ArgumentList is constructed as one loosely quoted string; verify the exact target behavior and the API contract.
Trace before changing quoting
PowerShell 7.3 added native parameter-binding tracing. It can show how PowerShell binds command arguments during parsing, which is helpful when a command differs from what a script author expects.
Trace-Command -Name ParameterBinding -PSHost -Expression {
some-native-tool.exe --name 'two words' --empty ''
}
Tracing is evidence about PowerShell’s binding path, not a complete substitute for observing the arguments inside the child process. The target can apply its own parser after process creation, and programs differ in their option conventions. Capture both sides: the PowerShell command and the target’s observed arguments. Redact secrets from verbose traces before sharing them.
When a command works in an interactive prompt but fails in CI, compare the actual runtime, platform, executable path, current directory, environment, and native argument mode. A wrapper function or alias can also change which command PowerShell resolves. Get-Command some-native-tool -All helps identify the command resolution chain; an absolute executable path is appropriate when the deployment contract requires one exact binary.
A practical migration checklist
Inventory native invocations instead of mechanically rewriting every command. Prioritize calls that pass empty strings, nested quotes, JSON or regular-expression text, paths with spaces, arrays, or values that contain shell punctuation. Record the documented behavior expected by each target program. Then run each case against a diagnostic executable and the real program’s safe validation or dry-run mode.
If a regression is isolated to a legacy target, first look for a supported structured interface or a version-specific argument mode. If a scoped compatibility setting is necessary, add a comment naming the executable and a test that fails when arguments are altered. Do not set the preference at profile startup: profiles are host-specific and can make automated jobs depend on a user’s machine configuration.
Finally, preserve native exit status separately from argument correctness. A process can receive exactly the intended arguments and still return a nonzero exit code. Capture $LASTEXITCODE immediately after the native invocation if later native commands could overwrite it. Argument transport, output decoding, stderr policy, and exit-code policy are related integration concerns, but they are not the same failure mode.
Maintain a compatibility matrix for wrappers
If your script invokes another script host, batch file, or legacy executable, record the tested PowerShell version and operating system next to the invocation. A wrapper can resolve to different binaries on a developer machine and a CI runner, and a filename extension can select different argument handling in Windows mode. Include the exact executable path, relevant mode, sample argument values, and expected target-side argument list in the test fixture.
When a compatibility override is necessary, centralize it in a small adapter around that executable rather than changing $PSNativeCommandArgumentPassing for an entire job. The adapter should accept typed values, build one argument list, invoke one target, preserve its exit code, and have a regression test that checks empty strings and embedded quotes. That narrows the blast radius when a future PowerShell release or target-program update changes the argument contract.
Do not use a manual transcript as the only test artifact. PowerShell may render an invocation in a form that cannot distinguish a literal quote from parser syntax. Capture the child process’s received argument count and values, and compare them with an expected array. Keep sensitive arguments out of those fixtures and logs; use a non-secret token such as ARG_SENTINEL_1 when validating position and quoting.
Related:
- PowerShell Native Command Errors: Exit Codes, Streams, and Catchable Failures
- PowerShell Pipelines: Object Enumeration and Input Binding
Sources: