name: powershell description: Enterprise PowerShell coding standards. Use when writing, reviewing, or generating any PowerShell code, creating PS1 scripts or functions, debugging PowerShell, or asked to help with PowerShell. Enforces best practices for structure, error handling, security, performance, and output patterns.
This skill enforces enterprise-grade PowerShell standards when writing, reviewing, or generating any PowerShell code. Apply all rules below automatically — do not wait to be asked.
Triggers
- "write a PowerShell script"
- "create a function in PowerShell"
- "review this PowerShell"
- "generate a PS1 file"
- "help me with PowerShell code"
Resource Files
Load these when needed for detailed patterns, examples, and gotchas:
| File | When to load |
|---|---|
resources/common-patterns.md |
Need a code pattern (script skeleton, error handling, collections, splatting, regex, pipeline functions, string building, hashtable lookups, safe property accessor helper, module structure, ShouldProcess+Force, etc.) |
resources/compatibility-and-clm.md |
Need PS5.1 vs PS7+ compatibility details, String.Split() changes, $IsWindows portability, or writing code that may run under AppLocker/WDAC (CLM) |
resources/traps-and-gotchas.md |
Debugging unexpected behaviour around nulls, booleans, comparison operators, pipeline output pollution, $PSBoundParameters, VerbosePreference in modules, PS class limitations, or defensive null-safe patterns (property access guards, ContainsKey null guards, hashtable key/value guards, null loop entries, Add-Member verification, array double-wrapping) |
resources/get-help-and-get-member.md |
Using an unfamiliar cmdlet or object — discover parameters, properties, and online docs before writing code |
resources/performance-patterns.md |
Writing performance-sensitive code — benchmarked patterns for collections, strings, filtering, object creation, hashtable lookups, large file processing, function call overhead |
resources/api-and-web.md |
Calling REST APIs — authentication, pagination, rate limiting/retry, JSON depth, credential management, PSReadLine history protection |
resources/security-hardening.md |
Security controls — CLM enforcement (WDAC vs AppLocker), PowerShell logging (Module/Script Block/Transcription), Protected Event Logging, JEA, code signing, script injection prevention |
Quick Reference
| Area | Rule |
|---|---|
| Structure | #Requires → help → param() → Functions → Main → Cleanup |
| Strict Mode | Set-StrictMode -Version Latest always |
| Indent | 4 spaces, ≤120 chars per line |
| Encoding | UTF-8 without BOM; always specify -Encoding utf8NoBOM explicitly |
| Naming | PascalCase functions/params, camelCase locals |
| Functions | Always [CmdletBinding()]; Verb-Noun singular nouns |
| State-changing | SupportsShouldProcess + $PSCmdlet.ShouldProcess() |
| Output | Emit [PSCustomObject]; never Write-Host for data |
| Errors | try/catch with -ErrorAction Stop; never empty catch |
| Arrays | Capture foreach output directly — never += in loops |
| WMI | Get-CimInstance not Get-WmiObject |
| Events | Get-WinEvent not Get-EventLog |
| Secrets | PSCredential/SecureString/SecretManagement vault — no plaintext |
| Native cmds | Check $LASTEXITCODE after native executables; use try/catch for cmdlets |
| Aliases | No aliases in scripts — always full cmdlet names |
| Paths | Join-Path $PSScriptRoot 'file.csv' — never assume working directory |
| CLM | Check $ExecutionContext.SessionState.LanguageMode; avoid .NET::new(), Add-Type if CLM is possible |
Critical Rules
These are non-negotiable. Apply them to every script and function.
Set-StrictMode -Version Latestat the top of every script (not inside functions).[CmdletBinding()]on every advanced function, no exceptions.-ErrorAction Stopon every cmdlet call inside atryblock, or set$ErrorActionPreference = 'Stop'for the scope. Non-terminating errors do NOT triggercatchwithout this.- Never empty
catchblocks. Always log at minimumWrite-WarningorWrite-Error. - Save
$_immediately at the start of a catch block:$err = $_ - No
+=in loops. Captureforeachoutput directly, or use[System.Collections.Generic.List[PSObject]]::new()+.Add()in FullLanguage mode only. - No
Invoke-Expressionon untrusted or constructed input — ever. - No plaintext credentials in code, parameters, or log output.
- No aliases in scripts (
%,?,gci,ft, etc.). - No
Format-*mid-pipeline — emit objects; let the caller format. SupportsShouldProcesson any function that modifies state (files, registry, AD, etc.).- No
begin/process/endat script top level — only inside pipeline-aware functions. - No positional parameters in scripts — always use named parameters (e.g.,
Get-Item -Path $pnotGet-Item $p). - Always specify
-Encodingon file I/O cmdlets — default encoding differs between PS5.1 and PS7+.
Anti-Patterns
| Anti-Pattern | Replace With |
|---|---|
Get-WmiObject |
Get-CimInstance |
Get-EventLog |
Get-WinEvent -FilterHashtable @{...} |
$array += $item in loops |
Capture foreach output, or [List[PSObject]]::new() + .Add() |
Write-Host for data output |
Write-Output / emit objects |
Format-Table mid-pipeline |
Emit objects; format at end |
Invoke-Expression $cmd |
Parameterised calls / splatting |
| Plaintext password in param | [PSCredential] + SecretManagement |
Empty catch {} |
Always log or re-throw |
$? after native executables |
$LASTEXITCODE (for cmdlets use try/catch) |
ConvertTo-Json without -Depth |
Always ConvertTo-Json -Depth 10 (default 2 silently truncates) |
$global: for cross-function state |
$script: scope — contained to the script file |
| Building HTML without encoding | Regex replace for 5 HTML special chars (CLM-safe); HttpUtility in FullLanguage only |
Read-Host for required input |
[Parameter(Mandatory)] |
Aliases (gci, %, ?) |
Full cmdlet names |
begin/process/end at script top |
Only inside pipeline-aware functions |
Positional parameters (Get-Item $p) |
Named parameters (Get-Item -Path $p) |
Out-Null in hot paths |
[void](...) or $null = ... (no pipeline overhead) |
Where-Object when source has -Filter |
Use -Filter on the source cmdlet |
ForEach-Object { $_.Prop } for single property |
Select-Object -ExpandProperty Prop |
New-Object PSObject -Property @{} |
[PSCustomObject]@{} (3x faster, cleaner) |
[array]::new() or List[T]::new() in CLM |
Capture foreach output directly |
Add-Type in potentially CLM environments |
Cmdlet-based alternatives |
continue inside ForEach-Object |
return (acts as continue in pipeline context) |
if ($array -eq $null) |
if ($null -eq $array) (left-side null check) |
if ($results) to test for empty collection |
if ($results.Count -gt 0) |
-Encoding utf8 without knowing PS version |
-Encoding utf8NoBOM (explicit, portable) |
"abc" -contains "ab" for substring |
"abc".Contains("ab") or "abc" -match "ab" |
-like "pattern\d+" (regex in glob) |
-match "pattern\d+" for regex patterns |
String "False" as a boolean |
Explicit -eq 'True' or -eq $true comparison |
$list.Add($item) on ArrayList |
[void]$list.Add($item) — .Add() returns the index to the pipeline |
New-Item/New-Object output leaked |
$null = New-Item ... or assign to variable |
Write-Output $x (usually) |
Just emit $x implicitly; use Write-Output -NoEnumerate only for arrays |
| Inconsistent output types per code path | Always emit the same type; use error stream for errors |
No [OutputType()] on functions |
Declare [OutputType([PSCustomObject])] on functions with defined output |
Invoke-RestMethod without $ProgressPreference |
Set $ProgressPreference = 'SilentlyContinue' at script top for non-interactive use |
Parameter named Verbose, Debug, WhatIf, etc. |
These are reserved by [CmdletBinding()] — rename to avoid conflicts |
Parameter named Error, Input, Host, Args |
These shadow automatic variables — use distinct names |
$string += "text" in loops |
-join operator (790× faster at scale) |
Nested Where-Object for cross-collection joins |
Hashtable lookup — O(n+m) vs O(n×m) |
FunctionsToExport = '*' in manifest |
Explicit function list — avoids ~15s import penalty |
"str".Split('ab') for multi-char splitting |
"str".Split([char[]]'ab') — portable across PS5.1 and PS7+ |
if ($IsWindows) without edition check |
$PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows |
Invoke-RestMethod -Authentication + manual Authorization header |
Use only one — -Authentication silently overrides the header |
-FollowRelLink without -MaximumFollowRelLink |
Always set -MaximumFollowRelLink to prevent infinite loops |
| Class method with implicit output | Class methods discard all output except return — assign or return explicitly |
Import-Module MyModule; [ClassType]::new() |
using module MyModule required for class types |
hidden property assumed private |
hidden properties ARE serialized by ConvertTo-Json |
$obj.Prop on object that may lack Prop |
if ($obj.PSObject.Properties['Prop']) { $obj.Prop } — safe under Set-StrictMode and avoids PropertyNotFoundException |
$hash[$key] without null-guarding the key |
if ($key) { $hash[$key] } — null key crashes Dictionary<TKey,TValue>; guard first |
$hash.ContainsKey($key) without null guard |
$key -and $hash.ContainsKey($key) — null argument throws on Dictionary<> types |
Loop body with no null guard on $item |
if ($null -eq $item) { continue } at top — API/pipeline collections can contain null entries |
| Building lookup without guarding key or value | Check if ($id -and $value) before $lookup[$id] = $value — null keys create silent corrupt entries |
| `$connector | Add-Member ...; $connector._Prop` |
return ,$array in function + @(Func) at call site |
Double-wrap bug: $items = @(Func) when function returns ,$arr gives a 1-element wrapper; use direct assignment $items = Func |
Review Checklist
Before finalising any generated PowerShell, verify:
-
Set-StrictMode -Version Latestpresent at script top -
[CmdletBinding()]on every advanced function -
-ErrorAction Stopon all cmdlet calls intryblocks (or$ErrorActionPreference = 'Stop'set) - No empty
catchblocks;$err = $_saved at catch start - No
+=inside loops for collection building - No aliases used anywhere
- No
Write-Hostused for data (only acceptable for interactive UI messaging) - No
Invoke-Expressionon dynamic/user-supplied input - No plaintext secrets in code or output
-
SupportsShouldProcesson all state-modifying functions -
finallyblock for any resource cleanup - Output is objects (
[PSCustomObject]), not pre-formatted strings -
Get-CimInstanceused instead ofGet-WmiObject -
$LASTEXITCODEchecked after every native executable call -
$PSScriptRootused for all paths relative to the script file - Parameter variables not mutated; copied to local variables first
- Lines ≤120 characters; 4-space indentation throughout
- No positional parameter usage — all parameters named explicitly
-
-Encoding utf8NoBOMspecified on all file I/O operations -
$nullis on the LEFT side of all null comparisons - Empty collection checked with
.Count -eq 0, not bareif ($collection) -
@($results)used when.Countis needed on cmdlet output -
return(notcontinue) used to skip items inForEach-Object - CLM-unsafe patterns (
.NET::new(),Add-Type) avoided if script may run under AppLocker/WDAC - PS7-only syntax (
??,? :,-Parallel) annotated or avoided if PS5.1 support required -
-Filterused on source cmdlets rather than downstreamWhere-Objectwhere possible -
-likeused for glob patterns,-matchused for regex — not mixed -
-contains/-inused for collection membership, not string substring checks -
[void]$list.Add(...)used when callingArrayList.Add()(it returns the index) - Intermediate cmdlets (
New-Item,New-Object, etc.) inside functions have their output suppressed or assigned - No parameter names clash with common parameters (
Verbose,Debug,WhatIf,Confirm,ErrorAction, etc.) - No parameter names shadow automatic variables (
Error,Input,Host,Args,This) -
[OutputType()]declared on functions that emit a defined object type -
$ProgressPreference = 'SilentlyContinue'set in non-interactive scripts that callInvoke-WebRequest/Invoke-RestMethod -
Write-Hostnot used for data — only for interactive UI messages - String building in loops uses
-join, not+=(790× slower at scale) - Cross-collection joins use hashtable lookup, not nested
Where-Object(O(n+m) vs O(n×m)) - Module manifest uses explicit
FunctionsToExportlist, not'*'(~15s penalty at import) -
String.Split()with multi-char argument uses[char[]]cast for portable behaviour -
$IsWindowsportability uses$PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows - External/API object property access is guarded:
if ($obj.PSObject.Properties['Prop'])before$obj.Prop(strict-mode-safe) - Hashtable keys are null-guarded before indexing:
if ($key) { $hash[$key] }not bare$hash[$key] -
ContainsKey()calls are null-guarded:$key -and $hash.ContainsKey($key) - Loop bodies guard against null items at the top:
if ($null -eq $item) { continue } - Lookup hashtable population guards both key and value before inserting:
if ($id -and $value) { $lookup[$id] = $value } -
Add-Membernote properties are verified before access:if ($obj.PSObject.Properties['_Name']) { $obj._Name } - Functions using
return ,$arrayconvention are called with direct assignment ($x = Func), NOT$x = @(Func)(double-wrap bug)