# PowerShell 5.1 traps that only show up in the admin run

Published 2026-09-23 · PowerShell, Windows, scripting · https://home.tigran.manukyan.dev/notes/powershell-51-traps.html

**TL;DR:** Windows PowerShell 5.1 pitfalls: BOM-less UTF-8 scripts are read as ANSI, a failed [xml] cast does not stop the script, $null becomes an empty string in .NET calls, unary-comma returns break pipelines, variable names are case-insensitive, and native exit codes must be checked.

I wrote a cleanup script and tested it with non-admin dry runs. Every real bug showed up in the elevated run. These are the ones worth remembering.

## Keep .ps1 files pure ASCII

Windows PowerShell 5.1 reads a UTF-8 file without a BOM in the system's ANSI code page. One non-ASCII character in a regex turned into three garbage characters and the pattern stopped matching.

## A failed cast doesn't stop the script

`[xml]$x = $text` on bad input writes an error and carries on with `$x` empty. The error message also includes the whole input, which can be megabytes of red text. Use `XmlDocument.LoadXml` in `try/catch` and truncate the message.

## $null becomes "" in .NET calls

Passing `$null` to a .NET `string` parameter sends an empty string. When the API really needs NULL, pass `[NullString]::Value`.

## The unary comma and the pipeline

A function that ends in `return ,$list` sends the list as one object. `Get-Things | Where-Object { ... }` then filters the whole array as a single item. Assign to a variable first, then pipe.

## Variables are case-insensitive

A local `$folders = @()` inside a function hid the script-level `$Folders`. The loop ran over an empty array and the script reported success. Give config lists names that can't collide.

## Guards must fail closed

Start with `$blocked = $true` and clear it only when the check fully completes. My first version did the opposite, and a parse failure would have let it proceed.

## Check native exit codes

`pnputil /export-driver` fails if the target folder doesn't exist. Piped to `Out-Null`, nobody noticed, and the next line deleted the package without a backup. Check `$LASTEXITCODE` *and* that the file landed.
