PowerShell ShouldProcess: Make Destructive Functions Safe to Preview
Implement PowerShell WhatIf and Confirm correctly by gating each mutation through ShouldProcess and keeping preview paths side-effect free.
PowerShell’s ShouldProcess contract lets an advanced function describe a proposed action and its target before changing state. When implemented correctly, the common WhatIf and Confirm parameters give operators a way to preview or approve mutations without each function inventing a different confirmation switch. The contract is valuable for deployment, cleanup, account administration, and any script that changes external state.
The critical rule is simple: code that mutates state must run only when the current cmdlet or function’s ShouldProcess call returns true. Adding SupportsShouldProcess to CmdletBinding makes the common parameters available, but it does not automatically protect a body. The author must put the actual mutation behind the gate, and must keep planning and validation code free of hidden writes.
Declare the contract on the advanced function
An advanced function opts into the common confirmation parameters through CmdletBinding with SupportsShouldProcess. ConfirmImpact communicates how consequential the operation is relative to the caller’s ConfirmPreference. It is not a security boundary and should not be used to make dangerous code appear safe. Select an impact level that describes the operation, document what it means, and allow a caller to override policy using the supported common parameters.
function Remove-DeploymentArtifact {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string[]] $Path
)
process {
foreach ($candidate in $Path) {
$resolved = Resolve-Path -LiteralPath $candidate -ErrorAction Stop
foreach ($item in $resolved) {
$target = $item.ProviderPath
if ($PSCmdlet.ShouldProcess($target, 'Remove deployment artifact')) {
Remove-Item -LiteralPath $target -Recurse -Force -Confirm:$false -ErrorAction Stop
}
}
}
}
}
The function resolves each input and gates each concrete target separately. That makes a WhatIf report meaningful and permits an operator to approve one target without silently applying the same decision to a broad, opaque batch. This example is intentionally powerful; a production implementation should also enforce an allowed root, reject unexpected providers, and define how links and reparse points are handled before it is used against real data.
The nested Remove-Item also has confirmation support. Once the wrapper has made the ShouldProcess decision, the example disables a second prompt for that nested call to avoid duplicate interaction. The wrapper’s gate remains responsible for the preview and approval. If the wrapped operation is an external tool or a different provider, document which layer owns WhatIf and confirmation rather than assuming the common parameter automatically flows through every boundary.
Gate the mutation, not the entire workflow
Validation, planning, and discovery should usually run during WhatIf so the preview can show what would happen. Keep those steps read-only. Separate discovery from mutation, calculate the exact target, and call ShouldProcess with an accurate target and a verb phrase that describes the action. The target should be specific enough to review, not a generic word such as Everything.
function Set-ReleasePointer {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Environment,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $ReleaseId
)
$target = '{0}/{1}' -f $Environment, $ReleaseId
if (-not (Test-ReleaseExists -ReleaseId $ReleaseId -ErrorAction Stop)) {
throw "Release does not exist: $ReleaseId"
}
if ($PSCmdlet.ShouldProcess($target, 'Move release pointer')) {
Invoke-ReleasePointerUpdate -Environment $Environment -ReleaseId $ReleaseId -ErrorAction Stop
}
}
The validation call occurs before the gate, so WhatIf still catches an unknown release identifier. That call must not create, reserve, or alter a release. If validation itself makes a remote request, explain that WhatIf is a dry run of the mutation, not necessarily a guarantee of zero network traffic. A preview should state all read-only work that can occur and should avoid surprising users with costly discovery.
If a plan consists of multiple independent writes, gate each write at the granularity that the operator must approve. If the operation is atomic and genuinely indivisible, one gate may accurately represent it. Do not put a loop of many unrelated changes behind a single generic confirmation unless the user explicitly expects one all-or-nothing approval. Conversely, do not prompt once per low-level file when the real operation is one documented transaction.
Treat WhatIf as a control path to test
WhatIf asks the cmdlet to describe the operation it would have performed. It is not a rollback mechanism and it does not make unsafe planning code harmless. Any mutation that happens before ShouldProcess still happens during a WhatIf run. This includes temporary files, cloud API writes, credential changes, and child processes launched to perform an action.
Test WhatIf with a fixture that makes the mutation observable. Assert that the preview includes the intended target and action, and separately assert that the fixture state is unchanged. Test the normal path with a disposable target and confirm that the mutation occurs exactly once. A message saying What if: is not enough evidence if an earlier statement already changed state.
Also test an input that fails validation, an empty pipeline, multiple pipeline records, an inaccessible target, and a downstream command failure. Record which operations are performed in each case. A well-designed function validates early, provides a useful preview, makes no write in preview mode, and reports a real error if the approved operation fails.
WhatIf output is an operator-facing interface. Include stable identifiers and enough context to tell similar targets apart, but do not print tokens, secret-bearing connection strings, or full private data. If multiple resource types can be mutated, include the type and scope in the target or action description so the preview is not ambiguous.
Use ConfirmImpact and caller policy deliberately
ConfirmImpact and ConfirmPreference work together to decide when a confirmation prompt is normally required. An explicit Confirm parameter can request or suppress confirmation, while WhatIf selects preview behavior. These controls are designed for interactive safety and script composition; they are not a substitute for authorization checks, least privilege, or server-side conditional updates.
Choose impact consistently. A function that deletes a local disposable cache may warrant a different default than one that removes production customer data. If every helper claims high impact, the shell can become prompt-fatigued and users may approve without reading. If a destructive operation claims low impact to avoid prompts, the interface misrepresents risk and undermines the shared policy.
Unattended automation must not depend on an interactive confirmation prompt. Decide how the job should run: an explicit WhatIf stage for preview, followed by an approved execution stage with recorded inputs and appropriate credentials. Configure confirmation behavior deliberately and fail closed when the automation cannot establish the intended policy. Do not pipe dummy input to a prompt to make a task appear unattended-safe.
ShouldContinue is a separate, more direct interactive confirmation mechanism. It is intended for an additional question, often where a choice such as yes to all or no to all is meaningful. It is not an interchangeable implementation of the ShouldProcess contract. A function that uses only ShouldContinue may not participate correctly in WhatIf, common Confirm handling, or caller policy. Reserve it for a clearly justified secondary prompt and keep the main mutation behind ShouldProcess.
Compose wrappers without bypassing previews
A wrapper around another PowerShell cmdlet needs a clear ownership model. If the wrapper gates the high-level action, it may call an inner ShouldProcess-capable cmdlet in a way that prevents a redundant prompt. If the inner cmdlet owns the gate instead, the wrapper should pass the common parameter behavior through and avoid doing any related writes outside that call. Never assume that SupportsShouldProcess recursively instruments arbitrary functions.
External programs do not automatically understand PowerShell WhatIf. If a wrapper calls a CLI that deletes a resource, the wrapper must guard that invocation and report what it would have called. If the external program has its own preview flag, map the contract deliberately and test it. Environment variables such as WhatIfPreference affect PowerShell behavior, not the semantics of an unrelated executable.
For a multi-step operation, make the boundary explicit. One option is to prepare an immutable plan, show it, then apply the plan after a ShouldProcess decision for each resource. Another is to gate one transaction and pass the exact planned change to a transactional API. Avoid mixing plan recalculation and mutation such that a target changes between approval and execution; use conditional requests, version identifiers, or other provider-side safeguards when available.
Keep approvals narrow and race-aware
The target shown to an operator must correspond to what the code actually mutates. Resolve ambiguous names before prompting, constrain the provider, and use literal path handling when wildcard interpretation is not intended. For remote resources, include a stable resource identifier and environment or tenant in the target description. A confirmation string that names one object while the following operation acts on a newly resolved name can approve the wrong object.
There can be a time gap between displaying the proposed action and applying it. The target may change or be replaced in that gap. For high-impact work, retain an immutable identifier or version from planning and ask the remote system to apply the change only if that version still matches. ShouldProcess provides a human decision boundary, not a lock or transaction.
If a user’s approval changes the system, emit a concise result that can be correlated with the preview: what target was changed, which operation succeeded, and any resulting identifier. Keep audit records in the system’s approved logging path and avoid logging secrets. A preview and its execution should use the same targeting logic so the operator can compare intent with outcome.
Operational checklist
Declare SupportsShouldProcess on each advanced function that can mutate external state. Put every mutation behind a call to $PSCmdlet.ShouldProcess, make the target and action precise, and keep preview-time discovery read-only. Decide whether the wrapper or an inner cmdlet owns the confirmation boundary. Validate both the WhatIf no-write path and the normal path with observable fixtures.
PowerShell’s confirmation model is effective when it is treated as an API contract rather than decorative metadata. SupportsShouldProcess exposes the policy surface; the implementation must honor it. Preview the exact change, test that no mutation occurs during WhatIf, and rely on authorization and concurrency controls for guarantees the interactive confirmation system cannot provide.
Related:
- PowerShell Advanced Function Parameters: Validation as an Input Contract
- PowerShell Native Command Errors: Exit Codes, Streams, and Catchable Failures
Sources: