PowerShell Scripting skill for Codex
pwsh scripts that stop on errors and honour -WhatIf. Codex writes PowerShell as advanced functions with validated parameters, objects on the output stream and SupportsShouldProcess around every change, so destructive scripts can be previewed with -WhatIf. It sets errors to stop, makes native exit codes throw, avoids output pollution and array unrolling, packages modules with explicit exports, and proves the work with PSScriptAnalyzer and Pester 5.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- powershell-scripting Installs to ~/.agents/skills/powershell-scripting/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install powershell-scripting The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project powershell-scripting Installs to .agents/skills/powershell-scripting/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/powershell-scripting.zipunzip -o powershell-scripting.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) powershell-scripting PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $powershell-scripting, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes PowerShell 7 scripts and modules that behave predictably - advanced functions with CmdletBinding and parameter validation, pipeline input with process blocks, terminating errors and native exit codes, SupportsShouldProcess for -WhatIf, module manifests, PSScriptAnalyzer and Pester 5 tests - and avoids the output-pollution and array-unrolling traps. Use when writing or reviewing .ps1/.psm1 files, automating Windows, Azure or cross-platform admin tasks, porting Windows PowerShell 5.1 scripts to pwsh, or adding tests to PowerShell code.
Use it when
- Writing or reviewing .ps1, .psm1 or .psd1 files
- Automating Windows, Azure, Microsoft 365 or cross-platform admin tasks
- A script continues after errors or returns unexpected extra output
- Adding Pester tests or ScriptAnalyzer to PowerShell code
Use something else when
- You need desktop UI automation or hotkeys on Windows: use windows-automation
- The target is a POSIX shell on Linux or macOS: use bash-scripting
How to check the result
Invoke-ScriptAnalyzer reports nothing for changed files, Invoke-Pester passes, and state-changing scripts run with -WhatIf list exactly the intended changes.
The mistake it prevents
Assuming try/catch sees cmdlet failures; most cmdlet errors are non-terminating and skip the catch block unless ErrorAction is Stop.
A good PowerShell script is an advanced function or module: typed and validated
parameters, objects (not formatted text) on the output stream, failures that stop
execution, -WhatIf support on anything that changes state, and Pester tests plus a
clean PSScriptAnalyzer run.
Before writing
- Read
AGENTS.mdand existing scripts/modules for conventions (verb-noun names, logging, folder layout,#Requireslines). - Decide the host: PowerShell 7 (
pwsh, cross-platform, 7.4 LTS or newer) or Windows PowerShell 5.1 (powershell.exe, Windows only, needed for some legacy modules). Check with$PSVersionTable. Write for 7 unless a dependency forces 5.1, and state it with#Requires -Version 7.4. - Execution policy is a user and organisation setting. If a script will not run because of it, tell the user; never change it or suggest bypassing it yourself.
Script template
#Requires -Version 7.4
<#
.SYNOPSIS
Removes build artifacts older than a given age.
.EXAMPLE
./Remove-OldArtifact.ps1 -Path D:\builds -OlderThanDays 30 -WhatIf
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string] $Path,
[ValidateRange(1, 3650)]
[int] $OlderThanDays = 30
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true # non-zero exit codes from native tools throw (7.4+)
$cutoff = (Get-Date).AddDays(-$OlderThanDays)
$stale = Get-ChildItem -LiteralPath $Path -Directory |
Where-Object LastWriteTime -lt $cutoff
foreach ($dir in $stale) {
if ($PSCmdlet.ShouldProcess($dir.FullName, 'Remove directory')) {
Remove-Item -LiteralPath $dir.FullName -Recurse -Force
}
}
[pscustomobject]@{ Path = $Path; Removed = @($stale).Count; Cutoff = $cutoff }
SupportsShouldProcessgives-WhatIfand-Confirmfor free; wrap every state change in$PSCmdlet.ShouldProcess(...). Cmdlets called inside inherit-WhatIf.ConfirmImpact = 'High'prompts by default; callers pass-Confirm:$falsein automation. Run destructive scripts with-WhatIffirst and show the user the output before running for real.- Use
-LiteralPathfor user-provided paths so[and]are not wildcards. - Emit objects at the end; let the caller format them.
Errors
| Situation | Do |
|---|---|
| Cmdlet emits non-terminating errors (most file and AD cmdlets) | $ErrorActionPreference = 'Stop' script-wide, or -ErrorAction Stop per call, so try/catch sees them |
Native executable (git, az, kubectl) fails | $PSNativeCommandUseErrorActionPreference = $true (7.4+), or check $LASTEXITCODE right after the call |
| Expected, recoverable failure | try { } catch [System.IO.IOException] { } with a specific type |
| Your function detects bad state | throw with a clear message, or $PSCmdlet.ThrowTerminatingError(...) in advanced functions |
| Cleanup | finally { }; it runs on Ctrl+C too |
try {
$config = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json -AsHashtable
}
catch [System.Management.Automation.ItemNotFoundException] {
throw "Config file not found: $ConfigPath"
}
Inside catch, $_ is the ErrorRecord; $_.Exception.Message for the text and
$_.ScriptStackTrace for where it happened.
Pipeline functions
function Get-UrlStatus {
[CmdletBinding()]
[OutputType([pscustomobject])]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('Uri')]
[string[]] $Url,
[int] $TimeoutSec = 10
)
begin { $client = [System.Net.Http.HttpClient]::new(); $client.Timeout = [timespan]::FromSeconds($TimeoutSec) }
process {
foreach ($u in $Url) {
$resp = $client.GetAsync($u).GetAwaiter().GetResult()
[pscustomobject]@{ Url = $u; Status = [int]$resp.StatusCode }
}
}
end { $client.Dispose() }
}
'https://example.com', 'https://example.org' | Get-UrlStatus
- Without a
processblock, pipeline input is only the last item. - Loop inside
processover the parameter too, so both-Url a,band piping work. - Use approved verbs (
Get-Verb); PSScriptAnalyzer flags others.
Parallelism: ForEach-Object -Parallel { ... } -ThrottleLimit 8 runs in separate
runspaces; outer variables need $using:name, and module imports must be repeated
inside the block.
Output stream traps
- Every uncaptured value is output.
$list.Add($x)on anArrayListemits an index;New-Itememits the item. Suppress with$null = ...(fastest) or[void](...). A function’s “return value” is all of its output, not justreturn $x. - Single-item results unroll to a scalar and empty results become
$null; wrap with@(...)when you need.Count. To return an array intact:return , $array. $x -eq $nullfilters when$xis a collection; write$null -eq $x.Write-Hostgoes to the information stream, not the output stream; useWrite-Verbosefor diagnostics andWrite-Output(implicit) for data.ConvertTo-Jsondefaults to-Depth 2and silently truncates deeper objects; pass-Depth 10(or what the data needs).+=on arrays copies the whole array each time; collect with$items = foreach (...) { ... }or a[System.Collections.Generic.List[object]].
Modules
MyTools/
├── MyTools.psd1 # manifest
├── MyTools.psm1 # dot-sources Public/ and Private/
├── Public/Get-UrlStatus.ps1
├── Private/Invoke-Retry.ps1
└── tests/Get-UrlStatus.Tests.ps1
New-ModuleManifest -Path ./MyTools/MyTools.psd1 -RootModule MyTools.psm1 `
-ModuleVersion 1.0.0 -PowerShellVersion 7.4 -FunctionsToExport @('Get-UrlStatus')
List exported functions explicitly in FunctionsToExport (no wildcards): it keeps
private helpers private and makes module auto-loading fast. Publishing to the
PowerShell Gallery is outward-facing; do it only after the user confirms.
Secrets
Never hard-code credentials or write them to logs. Use Get-Credential
interactively, environment variables injected by CI, or the
Microsoft.PowerShell.SecretManagement module (Get-Secret -Name ApiToken -AsPlainText
only at the point of use). ConvertTo-SecureString -AsPlainText with a literal is not
secret storage.
Testing with Pester 5
BeforeAll {
$scriptPath = Join-Path $PSScriptRoot '../Remove-OldArtifact.ps1'
}
Describe 'Remove-OldArtifact' {
It 'removes nothing with -WhatIf' {
$root = New-Item -ItemType Directory -Path (Join-Path $TestDrive 'b')
New-Item -ItemType Directory -Path (Join-Path $root 'old') | Out-Null
(Get-Item (Join-Path $root 'old')).LastWriteTime = (Get-Date).AddDays(-90)
& $scriptPath -Path $root -WhatIf
Join-Path $root 'old' | Should -Exist
}
}
- Put setup in
BeforeAll/BeforeEach; code at the top level of aDescriberuns during discovery, not during the test run (Pester 5 behaviour change from v4). - Use
$TestDrivefor files andMockfor external commands (Mock Invoke-RestMethod { @{ status = 'ok' } }).
Verification
Invoke-ScriptAnalyzer -Path . -Recurse -Settings PSGallery
Invoke-Pester -Path ./tests -Output Detailed
- PSScriptAnalyzer: no errors or warnings in changed files (suppress only with
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(...)]plus a justification). - Pester passes on every target (pwsh on Linux/macOS/Windows if cross-platform).
- State-changing scripts run with
-WhatIfshow exactly the intended changes. - Paths built with
Join-Path, no hard-coded\separators in cross-platform code.
Installing modules (Install-PSResource Pester, PSScriptAnalyzer) needs network access
and may need approval.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "PowerShell Scripting"
short_description: "pwsh scripts that stop on errors and honour -WhatIf"
default_prompt: "$powershell-scripting Write a PowerShell script that cleans up old build folders with a -WhatIf option" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.