Why PowerShell Needs Specific AI Rules
PowerShell is the most misunderstood scripting language because it looks like Bash but works completely differently. Bash pipes text between commands; PowerShell pipes objects. Bash uses exit codes for errors; PowerShell uses exceptions and error records. AI assistants trained on Bash and Python generate PowerShell that ignores these fundamentals — parsing text output with regex instead of accessing object properties, using exit codes instead of try/catch, and writing procedural scripts instead of pipeline-oriented functions.
The most common AI failures: using Write-Host instead of Write-Output (breaks the pipeline), parsing command output as text instead of using object properties, not using approved verb-noun cmdlet naming, ignoring $ErrorActionPreference and try/catch, creating scripts instead of modules, and not using Pester for testing.
PowerShell is used for Windows administration, Azure automation, CI/CD, and cross-platform scripting (PowerShell 7). These rules apply to all contexts, with notes for Windows-specific and cross-platform differences.
Rule 1: Object Pipeline, Not Text Parsing
The rule: 'PowerShell passes objects through the pipeline, not text. Access properties directly: (Get-Process).Name — never Get-Process | Select-String "name". Use Select-Object for projection, Where-Object for filtering, ForEach-Object for transformation, Sort-Object for ordering, Group-Object for grouping. Never parse text output with regex when object properties are available.'
For output: 'Use Write-Output (or implicit output) for pipeline data — never Write-Host for data that other commands might consume. Write-Host writes to the console only and breaks the pipeline. Use Write-Verbose for diagnostic messages, Write-Warning for warnings, Write-Error for non-terminating errors.'
This is the fundamental PowerShell rule. AI writes `Get-Service | findstr Running` (Bash-style text parsing) instead of `Get-Service | Where-Object Status -eq Running` (PowerShell object pipeline). The object version is faster, more reliable, and composes with other cmdlets.
- Access object properties: $process.Name — not text parsing with regex
- Where-Object for filtering — Select-Object for projection
- Write-Output for pipeline data — Write-Host only for user display
- Write-Verbose for diagnostics — Write-Warning for warnings
- Never pipe to findstr, grep, or awk — use PowerShell cmdlets
AI writes `Get-Service | findstr Running` (Bash-style text parsing). Correct: `Get-Service | Where-Object Status -eq Running`. The object version is faster, more reliable, and composes with other cmdlets.
Rule 2: Verb-Noun Naming and Approved Verbs
The rule: 'All functions and cmdlets use Verb-Noun naming: Get-UserReport, Set-Configuration, New-DatabaseBackup. Use only approved verbs from Get-Verb: Get, Set, New, Remove, Start, Stop, Invoke, Test, etc. Never use Create (use New), Delete (use Remove), or Run (use Invoke/Start). PascalCase for everything: function names, parameter names, variable names.'
For parameters: 'Use [CmdletBinding()] on all functions. Declare parameters with [Parameter()] attributes: Mandatory, Position, ValueFromPipeline, ValidateSet, ValidateNotNullOrEmpty. Use common parameter names: -Name, -Path, -Force, -WhatIf, -Confirm. Support ShouldProcess for destructive operations.'
AI assistants generate functions named `delete-old-files` or `run-backup` — neither uses approved verbs. The naming convention isn't optional in PowerShell — PSScriptAnalyzer flags unapproved verbs, and modules with them generate warnings on import.
Rule 3: Error Handling
The rule: 'Set $ErrorActionPreference = "Stop" at the top of scripts to make all errors terminating. Use try/catch/finally for error handling — not $? or $LASTEXITCODE (those are for external command interop only). Catch specific exception types when possible: catch [System.IO.FileNotFoundException]. Use -ErrorAction Stop on individual cmdlets when $ErrorActionPreference shouldn't be global.'
For external commands: 'When calling .exe or native commands, check $LASTEXITCODE after execution: if ($LASTEXITCODE -ne 0) { throw "Command failed with exit code $LASTEXITCODE" }. PowerShell doesn't automatically throw on external command failure — you must check manually.'
For logging: 'Use Write-Error for non-terminating errors. Use throw for terminating errors. Use $PSCmdlet.ThrowTerminatingError() in advanced functions for rich error records. Always include context in error messages: what operation failed, what the input was, and what the expected behavior was.'
Set $ErrorActionPreference = 'Stop' at script top. Without it, PowerShell silently continues after errors — just like Bash without set -e. One line turns silent failures into caught exceptions.
Rule 4: Module Structure
The rule: 'Organize code as PowerShell modules, not standalone scripts. A module has: MyModule.psm1 (function definitions), MyModule.psd1 (module manifest), Public/ (exported functions), Private/ (internal helpers). Use dot-sourcing in .psm1 to load function files. Export only public functions in the manifest: FunctionsToExport = @("Get-Report", "Set-Config").'
For the manifest: 'Include: RootModule, ModuleVersion, Author, Description, FunctionsToExport, RequiredModules. Never export all functions with FunctionsToExport = "*" — be explicit. Pin RequiredModules versions for reproducibility.'
For scripts: 'Scripts (.ps1) are entry points that import modules and orchestrate. They handle parameters, configuration, and calling module functions. Business logic lives in the module, not the script. This separation enables testing — modules are testable, scripts are orchestration.'
- Module structure: .psm1 (functions) + .psd1 (manifest) + Public/ + Private/
- Export explicitly: FunctionsToExport = @('Get-X', 'Set-Y') — never '*'
- Scripts for orchestration — modules for business logic
- Pin RequiredModules versions — reproducible across environments
- One function per file in Public/ and Private/ directories
Rule 5: Testing with Pester
The rule: 'Use Pester 5 for all testing. Use Describe for test suites, Context for scenarios, It for individual tests. Use Should assertions: Should -Be, Should -BeExactly, Should -Throw, Should -HaveCount. Mock external dependencies with Mock: Mock Get-Date { return [DateTime]"2026-01-01" }. Use BeforeAll/BeforeEach for setup.'
For test organization: 'Mirror the module structure in tests/: tests/Public/Get-Report.Tests.ps1, tests/Private/Format-Data.Tests.ps1. Name test files with .Tests.ps1 suffix. Run with Invoke-Pester -Output Detailed. Generate code coverage with Invoke-Pester -CodeCoverage.'
For integration tests: 'Tag tests with -Tag "Integration" for tests that need external resources. Run unit tests in CI on every commit, integration tests on a schedule. Use TestDrive: for temporary file operations — it's automatically cleaned up after each test.'
Pester 5's Describe/Context/It + Should assertions + Mock give PowerShell testing parity with Jest or pytest. Test modules, not scripts. Use TestDrive: for automatic temp file cleanup.
Complete PowerShell Rules Template
Consolidated rules for PowerShell projects.
- Object pipeline: Where-Object, Select-Object — never text parsing with regex
- Write-Output for data — Write-Host only for display — Write-Verbose for diagnostics
- Verb-Noun naming with approved verbs — PascalCase everything — [CmdletBinding()]
- $ErrorActionPreference = 'Stop' — try/catch for errors — check $LASTEXITCODE for .exe
- Module structure: .psm1 + .psd1, Public/ + Private/, explicit FunctionsToExport
- Pester 5: Describe/Context/It, Should assertions, Mock for dependencies
- PSScriptAnalyzer in CI — zero warnings — custom rules for project conventions
- Support -WhatIf and -Confirm on destructive operations — ShouldProcess