You are probably familiar with the concept of the “black box” or “flight data recorder” that is onboard all commercial airliners. This is the piece of the aircraft most critically sought after in a crash investigation as it contains detailed recordings of everything happening onboard the aircraft up until the accident.
PowerShell (version 5+) has a similar feature! In a previous post, I wrote about PowerShell ConsoleHost_History, but PowerShell transcripts are even better (when they are properly enabled!)
What are PowerShell Transcripts?
PowerShell transcripts are plain‑text logs that capture everything typed in an interactive PowerShell session and all text written back to the console (inputs and outputs). You can start/stop them manually with Start-Transcript
/ Stop-Transcript
, or turn them on automatically across machines via Group Policy / Intune. When enabled, each session writes a file named like PowerShell_transcript.<COMPUTER>.<random>.<timestamp>.txt
(by default into the user’s Documents folder). Enabling the invocation header adds a per‑command timestamp line to the log—gold for timelines. Read more: Microsoft Learn
TL;DR: Transcripts = “what the user did and what PowerShell showed,” with host/process metadata and (optionally) per‑command timestamps. They’re easy to enable and easy to collect at scale.
Why Responders Should Care
Rich context: Header includes username, “RunAs” user, computer name, host application (e.g.,
powershell.exe
, ISE), process ID, PowerShell version, and start/stop times—handy for tying activity to processes and accounts.Actual outputs: Unlike
ConsoleHost_history.txt
/PSReadLine history, transcripts preserve outputs, not just commands—useful when attackers enumerate, dump configs, or test connectivity.Fleet‑wide coverage: Enabling the Turn on PowerShell Transcription policy is functionally the same as running
Start-Transcript
for every session. You can also force Include invocation headers to get per‑command timestamps.Cross‑version story: PowerShell 7 (“PowerShell Core”) also supports these policies via its own Administrative Templates (“PowerShell Core”).
Where They Live (and How They’re Named)
It’s imperative to know the various locations Transcripts may exist in your environment so that you’re prepared to acquire them in an incident. Tools like KAPE and Velociraptor have solid insights into the most common locations of Transcripts and many other forensic artifacts.
Default (manual Start-Transcript
):
$HOME\Documents\PowerShell_transcript.<COMPUTER>.<random>.<timestamp>.txt
You may also find them in other locations for various reasons. For instance, I have observed that when SYSTEM
runs PowerShell (whether for legitimate or illegitimate reasons), it sometimes generates a transcript in C:\Windows\System32
.
Policy‑controlled output directory (recommended):
Set a central, write‑only path (e.g., \\logshare\psx\
) so logs persist if the endpoint is wiped. Mandiant recommends a restricted, write‑only share to reduce tampering and lateral visibility.
Find the configured drop‑off path (Windows PowerShell 5.1):
# HKLM takes precedence over HKCU
$paths = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription',
'HKCU:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription'
$cfg = foreach($p in $paths){ if(Test-Path $p){ Get-ItemProperty $p } }
$cfg | Select-Object PSPath, OutputDirectory, EnableTranscripting, EnableInvocationHeader
PowerShell 7+:
In PS7+, policies live under ...Microsoft\PowerShellCore\Transcription
.
Key names you’ll see: EnableTranscripting
, EnableInvocationHeader
, OutputDirectory
(also present in PowerShell’s JSON config model).
Enable It Right (GPO / Intune / Registry)
Group Policy path (Windows PowerShell):Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on PowerShell Transcription
Enabling this is equivalent to starting a transcript for every session. Set Include invocation headers and a central Output directory.
PowerShell 7 (“PowerShell Core”):
Install/use the PowerShell Core ADMX templates and configure the same setting under the PowerShell Core nodes.
One‑liner to verify via registry (WinPS 5.1):
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' ` -ErrorAction SilentlyContinue | Select EnableTranscripting, EnableInvocationHeader, OutputDirectory
Intune: Configure “Turn on PowerShell Transcription” and set Include invocation headers + Transcript output directory in the profile. Read more about this approach here.
Field Collection & Triage
Quick hunt for local transcripts (note, expects unchanged default paths):
# Assumes default locations
Get-ChildItem -Path C:\Users -Filter 'PowerShell_transcript*.txt' -Recurse -ErrorAction SilentlyContinue |
Select-Object FullName, Length, LastWriteTime | Sort-Object LastWriteTime -Desc
For an even more robust version, see this example.
Reading the Header (What You Get for Free)
Each transcript begins with a header like:
**********************
Windows PowerShell transcript start
Start time: 20250814113458
Username: ERIC-PC\eric
RunAs User: ERIC-PC\eric
Configuration Name:
Machine: ERIC-PC (Microsoft Windows NT 10.0.26100.0)
Host Application: C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe
Process ID: 58464
PSVersion: 5.1.26100.4768
PSEdition: Desktop
PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.26100.4768
BuildVersion: 10.0.26100.4768
CLRVersion: 4.0.30319.42000
WSManStackVersion: 3.0
PSRemotingProtocolVersion: 2.3
SerializationVersion: 1.1.0.1
**********************
<actual commands will follow>
One of my favorite datapoints in the header is the Host Application
field which identifies the process that launched the PowerShell session. While in most cases this will be powershell.exe
or pwsh.exe
, unusual values—like rundll32.exe
, mshta.exe
, or a line-of-business app executable—can be a goldmine for investigators.
Spotting a suspicious or unexpected host process can reveal cases where PowerShell was spawned as part of a malicious execution chain (for example, malware using rundll32.exe
to run inline PowerShell code). This insight is valuable because it can:
Expose defense evasion — Attackers often hide PowerShell inside non-obvious parent processes to blend in with legitimate activity.
Provide lead evidence for lateral movement or persistence — An odd host process could indicate exploitation of an application vulnerability or abuse of a scheduled task/service.
Help correlate with other telemetry — Matching the host process name and PID to EDR or Sysmon logs can quickly expand the picture of how and when the PowerShell session was launched.
Pro Tip!
If wsmprovhost.exe
appears as the Host Application
, it means the PowerShell session was started via WinRM (PowerShell Remoting). This process hosts remote shells on the target and runs under the connecting user’s context. It’s normal for admin tasks and automation, but suspicious if seen at odd times, from unusual IPs, or tied to accounts that don’t typically perform remote management—often a sign of lateral movement.
Reading the Transcript
The standard expected output of the transcript itself will be every command that was executed and the output of that command.
# <header removed for brevity>
PS C:\Users\jdoe> Get-Date
Thursday, August 14, 2025 2:02:36 PM
PS C:\Users\jdoe> Get-Process | Select-Object -First 3
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
563 36 14692 32120 1.92 1056 1 chrome
427 24 10240 22144 0.33 3420 1 explorer
308 20 8232 17896 0.05 8124 1 powershell
PS C:\Users\jdoe> ipconfig
Windows IP Configuration
Ethernet adapter Ethernet0:
Connection-specific DNS Suffix . : contoso.local
IPv4 Address. . . . . . . . . . . : 10.1.2.55
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 10.1.2.1
PS C:\Users\jdoe> exit
**********************
Windows PowerShell transcript end
End time: 2025-08-14 14:03:10
**********************
If invocation headers are enabled, you’ll also see per‑command timestamp lines before each command—perfect for minute‑by‑minute timelines.
# <header removed for brevity>
**********************
Command start time: 20250814140236
**********************
PS C:\Users\jdoe> Get-Date
Thursday, August 14, 2025 2:02:36 PM
**********************
Command start time: 20250814140242
**********************
PS C:\Users\jdoe> Get-Process | Select-Object -First 3
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
563 36 14692 32120 1.92 1056 1 chrome
427 24 10240 22144 0.33 3420 1 explorer
308 20 8232 17896 0.05 8124 1 powershell
**********************
Command start time: 20250814140252
**********************
PS C:\Users\jdoe> ipconfig
Windows IP Configuration
Ethernet adapter Ethernet0:
Connection-specific DNS Suffix . : contoso.local
IPv4 Address. . . . . . . . . . . : 10.1.2.55
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 10.1.2.1
**********************
Command start time: 20250814140308
**********************
PS C:\Users\jdoe> exit
**********************
Windows PowerShell transcript end
End time: 2025-08-14 14:03:10
**********************
Example: Reconstructing an Intrusion from a Transcript
Look for telltale sequences such as:
Recon:
Get-ADUser
,Get-LocalGroupMember
,whoami /all
Payload fetch/exec:
IEX (New-Object Net.WebClient).DownloadString(...)
Defense evasion:
Set-MpPreference -DisableRealtimeMonitoring $true
, adding AV exclusionsPersistence:
schtasks /create ...
orNew-ItemProperty ... Run
keysExfil test:
Invoke-WebRequest
to attacker infra
Tie the per‑command timestamps to 4104/4103 events and process trees (EDR) to validate.
Limitations & Gotchas (Know Before You Lean on It)
Order & completeness: Some host‑written text (e.g.,
Write-Host
) can appear out of order relative to pipeline output. Edge cases can miss output if a script stops transcription mid‑formatting; Microsoft documents these limitations and a workaround (wrap in a script block and pipe toOut-Default
). Don’t panic—just correlate with event logs.Tamper‑able files: Transcripts are plaintext owned by the user that generated them. Use a write‑only central share and ship copies to your SIEM.
Scope: Transcripts record text in the PowerShell host. If a tool writes to a separate GUI or network socket without emitting console text, you won’t see it here.
Unmanaged or in-process PowerShell — Attackers can bypass transcription by loading the PowerShell engine via .NET (e.g., custom runspaces) or using portable/renamed binaries outside your managed installation. These sessions don’t honor Group Policy/registry logging settings and may evade both transcription and Script Block Logging. Read more about this here.
Version split: Historical nuance—Windows PowerShell vs PowerShell 7 policies live in different registry hives/nodes; ensure you configure both where PS7 is deployed.
Ops impact: If you force logging to an unavailable network share, some automation may hang or fail; test carefully before broad rollout. (If centralizing, ensure service accounts can write and you have offline buffering.)
Detection Notes (Hardening & Evasion)
Watch for logging disablement: Alert on changes to transcription/script‑block logging registry keys (Enable/Disable flips) under user or machine policy hives—classic defense‑evasion. Sigma rules exist specifically for this pattern, good example detection here.
Pair with Script Block Logging: Script block logs (
4104
) capture the de‑obfuscated code that executed. Even if an attacker obfuscates or uses-EncodedCommand
, you still get the decoded content in many cases. Read more about this here.Correlate transcripts with Script Block Logging for stealthy techniques — When paired with Event ID 4104 (decoded script blocks), transcripts can expose both the original cradle command (e.g.,
IEX (New-Object Net.WebClient).DownloadString(...)
) and the attacker’s obfuscated payload. This combined view gives analysts a before/after look that’s far harder for adversaries to fully hide, even when using the infamous “PowerShell Download Cradle”. Read about this technique here.
Enablement Cheat‑Sheet
Manual (ad‑hoc, only for current session):
Start-Transcript -OutputDirectory 'C:\Transcripts' -IncludeInvocationHeader
# ...do work...
Stop-Transcript
Manual (permanent, machine-wide):
New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' -Force | Out-Null
New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' -Name EnableTranscripting -Value 1 -PropertyType DWord -Force
New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' -Name EnableInvocationHeader -Value 1 -PropertyType DWord -Force
Fleet (policy):
Turn on PowerShell Transcription (+ include invocation headers) and point Output Directory at a central, write‑only share. (Windows PowerShell and PowerShell Core templates.)
Verify it’s working (PS 5.1 & PS 7):
$roots = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription',
'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\Transcription'
$roots | ?{ Test-Path $_ } | %{
[pscustomobject](Get-ItemProperty $_ |
Select EnableTranscripting, EnableInvocationHeader, OutputDirectory, PSPath)
}
Summary
Treat PowerShell transcripts as your shell’s flight recorder: enable them everywhere, include invocation headers, point output to a restricted, write‑only path, and alert on attempts to disable logging. In return you get per‑session, per‑command truth that stitches cleanly to 4103/4104 and EDR process trees, survives history clearing, and spotlights the telltale flags, Defender‑tamper switches, and LOLBIN abuse. They’re plaintext and not tamper‑proof—so hash, preserve, and corroborate—but for the cost of a single GPO/Intune setting they deliver incredible visibility. If you change one thing after reading this, make PowerShell transcription your default; future‑you (and your incident timeline) will thank you.
Want to get hands-on experience with this sort of analysis? Check out my hands-on courses which leverage these and many other artifacts for intrusion analysis in live-fire IR simulations.