Exit Codes
When it comes to IT, I like to keep a tidy house. You couldn't tell it from the state of my desk, but I like to keep things clean and organized. Using PowerShell is no different. When I am finished with a PowerShell session or module, I may want to perform some cleanup. Naturally, I want this to happen automatically. Let's look at some ideas and techniques for exit actions in several different contexts.
PowerShell
When you are done with a PowerShell session, you may want to perform some cleanup. This can be done by using the Register-EngineEvent
cmdlet to register an event handler for the PowerShell.Exiting
event. This event is triggered when the PowerShell engine is about to exit.
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
# Get the history
$history = Get-History
# Convert the history to JSON
$json = $history | Select-Object *,
@{Name="Computername";Expression = {[environment]::MachineName}} |
ConvertTo-Json -Depth 1
# Save the JSON to a file
$file = "C:\temp\History_{0:yyyyMMddhhmm}.json" -f (Get-Date)
$json | Out-File -FilePath $file -Encoding utf8
} | Out-Null
The cmdlet creates an event subscriber that watches for the designated event.
PS C:\> Get-EventSubscriber PowerShell.Exiting
SubscriptionId : 1
SourceObject :
EventName :
SourceIdentifier : PowerShell.Exiting
Action : System.Management.Automation.PSEventJob
HandlerDelegate :
SupportEvent : False
ForwardEvent : False
This is code you could put in your PowerShell profile to ensure it runs every time you start a new PowerShell session. This example captures the command history and saves it to a JSON file when the PowerShell session exits.
The one major caveat is that you must explicitly exit the PowerShell session by using the Exit
statement. If you close the PowerShell console window or terminate the process, the event will not be triggered, and the registered code will not run.
The other thing to keep in mind is that the code you want to run when the PowerShell session exits must be able to run very quickly. Here is another example.
This is code I would put in my PowerShell profile script.
$thisPS = Get-Process -Id $pid
#copied from PowerShell 7 member definition to get the
#information in Windows PowerShell
$cli = if ($IsWindows -or $PSEdition -eq 'Desktop') {
(Get-CimInstance Win32_Process -Filter "ProcessId = $($thisPS.Id)").CommandLine
} elseif ($IsLinux) {
$rawCmd = Get-Content -LiteralPath "/proc/$($this.Id)/cmdline"
$rawCmd.Substring(0, $rawCmd.Length - 1) -replace "`0", ' '
}
#create a custom information object
$global:psRunInfo = [PSCustomObject]@{
PSTypename = 'PSRunInfo'
ProcessID = $pid
Path = $thisPS.Path
CommandLine = $cli
PSEdition = $PSEdition
Start = $thisPS.StartTime
End = $null
Runtime = $null
Computername = [System.Environment]::MachineName
}
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
$Path = 'c:\temp\PSHistory.csv'
#update the end time and runtime
$stop = Get-Date
$run = New-TimeSpan -Start $thisPS.StartTime -End $stop
$global:psRunInfo.End = $stop
$global:psRunInfo.Runtime = $run
#parameters to splat to Export-Csv
$splat = @{
InputObject = $global:psRunInfo
Path = $Path
Append = $True
Encoding = 'utf8'
}
if ($IsCoreCLR) {
$splat.Add('IncludeTypeInformation', $True)
}
Export-Csv @splat
}
This code is written to run in Windows PowerShell and PowerShell 7. I originally tried putting this code in a separate script file and registering it.
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
C:\scripts\Save-PSRunInfo.ps1
} | Out-Null
But sometimes it wouldn't run. Based on my experience or at least for my use case, when I Get-CimInstance
in the script to retrieve the PowerShell process, I didn't get the results I expected. I suspect the PowerShell process is removed before my exit code has a chance to run. That led me to put most of the code in the profile script to create the object I want to export. The code in the event handler is just to export the object to a CSV file so it runs very quickly.
Now I have a log of PowerShell session history.
PS C:\> Import-Csv C:\temp\PSHistory.csv | Select -Last 2
ProcessID : 18624
Path : C:\Program Files\PowerShell\7\pwsh.exe
CommandLine : pwsh.exe -NoLogo -noProfile -NoExit -command "&{.
c:\scripts\miniprofile7.ps1}"
PSEdition : Core
Start : 8/20/2025 10:59:12 AM
End : 8/20/2025 11:01:29 AM
Runtime : 00:02:17.4875450
Computername : PROSPERO
ProcessID : 35464
Path : C:\Program Files\PowerShell\7-preview\pwsh.exe
CommandLine : "C:\Program Files\PowerShell\7-preview\pwsh.exe" -nologo
-noprofile
PSEdition : Core
Start : 8/20/2025 11:03:20 AM
End : 8/20/2025 11:07:07 AM
Runtime : 00:03:47.1900395
Computername : PROSPERO
I want to point out that I went beyond creating a simple text file. Creating structured data like JSON or CSV allows me to capture more information and more importantly, makes it easier to parse and analyze later. This is a great example of the benefit of using objects over text.