Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
August 22, 2025

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.

Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.