powershell

star 4

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.

dstreefkerk By dstreefkerk schedule Updated 6/3/2026

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.

  1. Set-StrictMode -Version Latest at the top of every script (not inside functions).
  2. [CmdletBinding()] on every advanced function, no exceptions.
  3. -ErrorAction Stop on every cmdlet call inside a try block, or set $ErrorActionPreference = 'Stop' for the scope. Non-terminating errors do NOT trigger catch without this.
  4. Never empty catch blocks. Always log at minimum Write-Warning or Write-Error.
  5. Save $_ immediately at the start of a catch block: $err = $_
  6. No += in loops. Capture foreach output directly, or use [System.Collections.Generic.List[PSObject]]::new() + .Add() in FullLanguage mode only.
  7. No Invoke-Expression on untrusted or constructed input — ever.
  8. No plaintext credentials in code, parameters, or log output.
  9. No aliases in scripts (%, ?, gci, ft, etc.).
  10. No Format-* mid-pipeline — emit objects; let the caller format.
  11. SupportsShouldProcess on any function that modifies state (files, registry, AD, etc.).
  12. No begin/process/end at script top level — only inside pipeline-aware functions.
  13. No positional parameters in scripts — always use named parameters (e.g., Get-Item -Path $p not Get-Item $p).
  14. Always specify -Encoding on 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 Latest present at script top
  • [CmdletBinding()] on every advanced function
  • -ErrorAction Stop on all cmdlet calls in try blocks (or $ErrorActionPreference = 'Stop' set)
  • No empty catch blocks; $err = $_ saved at catch start
  • No += inside loops for collection building
  • No aliases used anywhere
  • No Write-Host used for data (only acceptable for interactive UI messaging)
  • No Invoke-Expression on dynamic/user-supplied input
  • No plaintext secrets in code or output
  • SupportsShouldProcess on all state-modifying functions
  • finally block for any resource cleanup
  • Output is objects ([PSCustomObject]), not pre-formatted strings
  • Get-CimInstance used instead of Get-WmiObject
  • $LASTEXITCODE checked after every native executable call
  • $PSScriptRoot used 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 utf8NoBOM specified on all file I/O operations
  • $null is on the LEFT side of all null comparisons
  • Empty collection checked with .Count -eq 0, not bare if ($collection)
  • @($results) used when .Count is needed on cmdlet output
  • return (not continue) used to skip items in ForEach-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
  • -Filter used on source cmdlets rather than downstream Where-Object where possible
  • -like used for glob patterns, -match used for regex — not mixed
  • -contains/-in used for collection membership, not string substring checks
  • [void]$list.Add(...) used when calling ArrayList.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 call Invoke-WebRequest/Invoke-RestMethod
  • Write-Host not 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 FunctionsToExport list, not '*' (~15s penalty at import)
  • String.Split() with multi-char argument uses [char[]] cast for portable behaviour
  • $IsWindows portability 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-Member note properties are verified before access: if ($obj.PSObject.Properties['_Name']) { $obj._Name }
  • Functions using return ,$array convention are called with direct assignment ($x = Func), NOT $x = @(Func) (double-wrap bug)
Install via CLI
npx skills add https://github.com/dstreefkerk/claude-skills --skill powershell
Repository Details
star Stars 4
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator