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.

  • Languages
  • $powershell-scripting
  • 208 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- powershell-scripting

Installs to ~/.agents/skills/powershell-scripting/ for every project on this machine.

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

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.

~/.agents/skills/powershell-scripting/SKILL.md

--- name: powershell-scripting description: Writes PowerShell 7 scripts and modules that behave predictably - advanced functions with CmdletBinding and parameter validation, pipeline input with… license: MIT ---

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

  1. Read AGENTS.md and existing scripts/modules for conventions (verb-noun names, logging, folder layout, #Requires lines).
  2. 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.
  3. 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 }
  • SupportsShouldProcess gives -WhatIf and -Confirm for free; wrap every state change in $PSCmdlet.ShouldProcess(...). Cmdlets called inside inherit -WhatIf.
  • ConfirmImpact = 'High' prompts by default; callers pass -Confirm:$false in automation. Run destructive scripts with -WhatIf first and show the user the output before running for real.
  • Use -LiteralPath for user-provided paths so [ and ] are not wildcards.
  • Emit objects at the end; let the caller format them.

Errors

SituationDo
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 failuretry { } catch [System.IO.IOException] { } with a specific type
Your function detects bad statethrow with a clear message, or $PSCmdlet.ThrowTerminatingError(...) in advanced functions
Cleanupfinally { }; 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 process block, pipeline input is only the last item.
  • Loop inside process over the parameter too, so both -Url a,b and 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 an ArrayList emits an index; New-Item emits the item. Suppress with $null = ... (fastest) or [void](...). A function’s “return value” is all of its output, not just return $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 $null filters when $x is a collection; write $null -eq $x.
  • Write-Host goes to the information stream, not the output stream; use Write-Verbose for diagnostics and Write-Output (implicit) for data.
  • ConvertTo-Json defaults to -Depth 2 and 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 a Describe runs during discovery, not during the test run (Pester 5 behaviour change from v4).
  • Use $TestDrive for files and Mock for 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 -WhatIf show 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem