-ne against a list is a FILTER, so a two-entry denylist silently allows the value it blocks
collections · case
Symptom
Section titled “Symptom”A denylist check passes the thing it was written to block.
$forbidden = @("admin", "root")$user = "admin"
if ($forbidden -ne $user) { "ALLOWED" } else { "denied" }# ALLOWEDThe value is in the list. The comparison is spelled correctly. It still lets it through, and nothing warns you.
When the left operand is a collection, PowerShell’s comparison operators are
not booleans — they are filters. They return the matching elements, and the
if then applies truthiness to whatever came back.
$a = @("x","y","x")$r = ($a -eq "x")$r.GetType().Name # Object[] not Boolean@($r).Count # 2$r -join "," # x,x the matches themselves
($a -ne "x") -join "," # y the NON-matchesSo $forbidden -ne "admin" returns @("root") — a non-empty array — which is
truthy. The check is really asking “are there any entries that are not admin?”,
and the answer is yes.
The size dependency is what makes it dangerous
Section titled “The size dependency is what makes it dangerous”@("admin") -ne "admin" -> empty -> falsy -> "denied" correct@("admin","root") -ne "admin" -> @("root") -> truthy -> "ALLOWED" WRONG@() -ne "anything" -> empty -> falsy -> "denied" correctA denylist with one entry behaves correctly. Add a second entry and the check inverts. That is the worst possible failure curve: it works in the unit test, it works in the first deployment, and it breaks the day someone extends a config list.
Workaround
Section titled “Workaround”if ($forbidden -contains $user) { "denied" } else { "allowed" }# deniedRules that hold up:
- Membership is
-contains/-notcontains(collection on the left) or-in/-notin(collection on the right). Never-eq/-ne. - If you genuinely want a boolean from a comparison, keep the collection off the
left side, or wrap the result:
@($a -eq $x).Count -gt 0. - Be especially suspicious of
-neagainst any variable that might be a list.-eqat least fails toward “no match”;-nefails toward “allowed”.
Related trap in the same family
Section titled “Related trap in the same family”-contains does not do substring matching, which is what the name suggests
to anyone arriving from another language:
"hello" -contains "ell" # False"hello".Contains("ell") # TrueSo the operator that sounds like substring search is membership, and the operators that look like comparison are filters. Both surprises push in the direction of an accidental pass.
Nothing in the archive covers collection-side comparison semantics. The nearest
entries use -ne only against scalars ($LASTEXITCODE -ne 0), where the
behaviour is the expected boolean.
Environment
Section titled “Environment”$PSVersionTable: 5.1.26100.7705, Edition Desktop, Windows 11.