| name | pwsh-scripter |
| description | Use when writing PowerShell scripts, modules, or functions, especially for Azure automation tasks. Ensures code follows formatting standards, DRY principles, approved verb conventions, and cross-references Azure documentation for factual accuracy. Prevents parameter mismatches, hard-coded credentials, and incorrect Azure permission claims. |
PowerShell Scripter
Overview
This skill ensures PowerShell code generation follows professional standards: clean formatting, DRY principles, approved conventions, and factual accuracy (especially for Azure). Prevents common issues like parameter mismatches, redundant code, hard-coded credentials, and incorrect guidance.
Pressure Test Scenario
Setup: Ask to create a PowerShell module with multiple related functions (e.g., Get-AzureResource, Remove-AzureResource).
Baseline Failure (without skill):
- Parameter names differ between functions (
-ResourceGroupName vs -ResourceGroup)
- Validation logic duplicated across functions (not DRY)
- Incorrect Azure facts ("Owner role can't cancel subscriptions" — false)
- Non-approved verbs used (
Download-File instead of Get-File)
- Hard-coded credentials in plain text
- Unformatted code with inconsistent indentation
Success Criteria (with skill):
- ✅ Consistent parameter naming across module
- ✅ Shared helper functions for common logic
- ✅ Cross-referenced Azure facts via Microsoft Learn
- ✅ Uses approved PowerShell verbs only
- ✅ Clean formatting (One True Brace Style)
- ✅ Comment-based help for Get-Help support
- ✅ Parameter binding (no hard-coded values)
- ✅ Pipeline support (
ValueFromPipeline, process {} blocks)
- ✅ Proper error handling (
try-catch, -ErrorAction patterns)
- ✅ Pester tests included (
.Tests.ps1 file)
Core Rules
Code Quality Fundamentals
-
Clean Formatting (One True Brace Style) — Opening brace at end of statement line, closing brace on its own line. Use consistent indentation (4 spaces recommended). Space around operators (=, +, -eq). Auto-format via VSCode (Shift+Alt+F) before committing
-
Meaningful Variable Names — No single-letter variables in production code ($a, $b, $temp → $User, $ResourceGroup, $ValidationResult). Use plural for collections ($Users not $User for arrays). Pick ONE convention and stick to it: $camelCase or $PascalCase
-
Get-Help Compatible Comments — Every exported function MUST include comment-based help with .SYNOPSIS, .DESCRIPTION, .PARAMETER, .EXAMPLE. Users should get help via Get-Help <FunctionName> without reading source
-
No Hard-Coding — Use [CmdletBinding()] and param() blocks. NEVER hard-code credentials, server names, or paths. Use [Parameter(Mandatory=$true)] for required inputs. Leverage secure storage (Azure Key Vault, Windows Credential Manager) for secrets
-
Parameter Splatting for Readability — When passing 3+ parameters to a cmdlet, use splatting (@params hashtable) instead of inline parameter lists. Improves readability and maintainability
Module-Specific Rules
-
Parameter Consistency — Related functions in the same module MUST use identical parameter names for the same concepts (e.g., -ResourceGroupName everywhere, not -ResourceGroup in one function and -RGName in another)
-
DRY via Helper Functions — If validation, transformation, or API logic appears in 2+ functions, extract it into a private helper function. Keep private via Export-ModuleMember (only export public functions) OR place in Private/ folder for formal module structure. Use proper PowerShell verbs (no underscore prefixes)
-
Cross-Reference Azure Facts — Before stating Azure limitations, permissions, or API behavior, query MCP Microsoft Learn Server (mcp_microsoft_doc_microsoft_docs_search) to verify. If unavailable, prefix with "⚠️ Verify: " and link to docs
-
Approved Verbs Only — Use PowerShell approved verbs (Get-Verb list). Common mappings:
- ❌
Download → ✅ Get or Receive
- ❌
Process → ✅ Invoke or Update
- ❌
Check → ✅ Test
-
Pipeline Support — Functions that process collections MUST support pipeline input via [Parameter(ValueFromPipeline=$true)] and process {} block. Use $_ or $PSItem to reference current pipeline object. Enables idiomatic PowerShell: Get-Item | Process-Item | Format-Item
-
Explicit Error Handling — Use try-catch-finally for operations that may fail (network, file I/O, Azure API calls). Set -ErrorAction Stop on cmdlets inside try to catch non-terminating errors. Use throw for fatal errors, Write-Error for warnings. Always include context in error messages
-
Write Pester Tests — Every exported function MUST have a companion .Tests.ps1 file. Use BeforeAll for setup, Describe for grouping, It for test cases, and Should assertions. Test happy path, error cases, and edge conditions. See for comprehensive testing patterns, assertion operators, mocking, and CI/CD integration
Examples
Example 1: Clean Formatting
❌ Unformatted (bad):
function Get-User{param([string]$Name)if($Name-eq""){throw "Name required"}Get-ADUser -Filter "Name -eq '$Name'"}
✅ Formatted (good):
function Get-User {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
if ($Name -eq "") {
throw "Name is required"
}
Get-ADUser -Filter "Name -eq '$Name'"
}
Example 2: Comment-Based Help
✅ With Get-Help support:
function Get-AzureVM {
<#
.SYNOPSIS
Retrieves an Azure VM from a resource group
.DESCRIPTION
Queries Azure Resource Manager for a VM by name within a specified resource group.
.PARAMETER ResourceGroupName
The name of the Azure resource group containing the VM
.PARAMETER VMName
The name of the virtual machine to retrieve
.EXAMPLE
Get-AzureVM -ResourceGroupName "rg-prod" -VMName "vm-web-01"
#>
param(
[Parameter(Mandatory = $true)]
[string]$ResourceGroupName,
[Parameter(Mandatory = $true)]
[string]$VMName
)
# ... implementation
}
Example 3: Meaningful Variable Names
❌ Cryptic:
$a = Get-AzResourceGroup
$b = $a | Where-Object { $_.Location -eq 'eastus' }
✅ Meaningful:
$AllResourceGroups = Get-AzResourceGroup
$EastUSResourceGroups = $AllResourceGroups | Where-Object { $_.Location -eq 'eastus' }
Example 4: No Hard-Coding
❌ Hard-coded credentials:
$username = "admin@contoso.com"
$password = ConvertTo-SecureString "P@ssw0rd!" -AsPlainText -Force
✅ Parameter binding:
function Connect-Service {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCredential]$Credential
)
}
Example 5: Parameter Splatting
❌ Long parameter list:
Send-MailMessage -To "admin@contoso.com" -From "noreply@contoso.com" -Subject "Alert" -SmtpServer "smtp.contoso.com" -Port 587 -UseSsl
✅ Splatting:
$mailParams = @{
To = "admin@contoso.com"
From = "noreply@contoso.com"
Subject = "Server Restart Alert"
SmtpServer = "smtp.contoso.com"
Port = 587
UseSsl = $true
}
Send-MailMessage @mailParams
Example 6: DRY with Helper Functions
✅ Good:
function Test-ResourceGroupExists {
param([string]$Name)
if (-not (Get-AzResourceGroup -Name $Name -EA SilentlyContinue)) {
throw "Resource group '$Name' not found"
}
}
function Get-AzureVM {
Test-ResourceGroupExists -Name $ResourceGroupName
# ... rest of function
}
Example 7: Pipeline Support
❌ Without pipeline support:
function Format-UserName {
param([Parameter(Mandatory=$true)][string[]]$Users)
foreach ($user in $Users) {
$user.ToUpper()
}
}
# Forces: Format-UserName -Users $allUsers
✅ With pipeline support:
function Format-UserName {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string]$User
)
process {
$User.ToUpper()
}
}
# Enables: $allUsers | Format-UserName
# Or: Get-ADUser | Select-Object -ExpandProperty Name | Format-UserName
Example 8: Error Handling
❌ No error handling:
function Get-RemoteData {
param([string]$ComputerName)
Invoke-RestMethod -Uri "https://$ComputerName/api/data"
}
# Network failures crash script with cryptic error
✅ With proper error handling:
function Get-RemoteData {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$ComputerName
)
try {
$response = Invoke-RestMethod -Uri "https://$ComputerName/api/data" -ErrorAction Stop
return $response
}
catch [System.Net.Http.HttpRequestException] {
Write-Error "Failed to connect to $ComputerName. Check network connectivity."
throw # Re-throw if fatal
}
catch {
Write-Error "Unexpected error retrieving data from ${ComputerName}: $_"
throw
}
}
Example 9: Pester Testing
Get-Planet.ps1:
function Get-Planet {
[CmdletBinding()]
param([string]$Name = '*')
$planets = @(
@{ Name = 'Mercury' }
@{ Name = 'Earth' }
@{ Name = 'Mars' }
) | ForEach-Object { [PSCustomObject]$_ }
$planets | Where-Object { $_.Name -like $Name }
}
Get-Planet.Tests.ps1:
BeforeAll {
. $PSScriptRoot/Get-Planet.ps1
}
Describe 'Get-Planet' {
Context 'no parameters' {
It 'lists all 3 planets' {
$allPlanets = Get-Planet
$allPlanets.Count | Should -Be 3
}
It 'returns objects with Name property' {
$planet = Get-Planet | Select-Object -First 1
$planet.Name | Should -Not -BeNullOrEmpty
}
}
Context 'with -Name filter' {
It 'filters based on planet name' {
$result = Get-Planet -Name 'Earth'
$result.Count | Should -Be 1
$result.Name | Should -Be 'Earth'
}
It 'returns empty for non-existent planet' {
$result = Get-Planet -Name 'Pluto'
$result | Should -BeNullOrEmpty
}
}
}
Run tests:
Invoke-Pester -Output Detailed ./Get-Planet.Tests.ps1
Tool Requirements & Constraints
- Optional: MCP Microsoft Learn Server for cross-referencing Azure facts (Rule 8)
- Without MCP: Prefix Azure claims with "⚠️ Verify:" and link to Microsoft Learn
- Recommended: PowerShell 7+ (core rules apply to PS 5.1 too)
- Platform: Works with GitHub Copilot (VSCode, CLI), GitHub Copilot coding agent
Supporting Resources
- reference.md — Detailed rationale, citations, troubleshooting, approved verb list, module structure patterns, pipeline mechanics, error handling strategies
- pester-reference.md — Comprehensive Pester testing guide: assertion operators, mocking patterns, CI/CD integration
Version: 3.0.0 (Spec-compliant) | Created: 2026-01-22 | Updated: 2026-01-22