PowerShell logging is one of those controls everyone agrees you should turn on. Attackers love PowerShell, the logs are free, and every hardening guide recommends them. So it’s tempting to open the policy, enable every PowerShell logging setting, and move on.
I came across exactly that configuration while untangling a policy conflict in Intune. Script Block Logging, invocation start and stop events, Module Logging for every module, and transcription, all switched on. The intent was right. But each of those settings has a cost, and some of them work against each other.
This article covers what each setting actually produces, where the hidden costs are, and the configuration I’d settle on. The costs fall into five areas: volume, performance, sensitive data, coverage gaps, and whether the logs ever leave the device. It’s also worth checking whether your settings are conflicting with each other before you change anything, which is how I found this configuration in the first place.
The four settings, and what each one produces
In Group Policy and Intune’s Administrative Templates, Windows PowerShell logging comes down to four settings:
| Setting | What it records | Where it goes |
|---|---|---|
| Script Block Logging | The code of every script block PowerShell processes (event 4104) | PowerShell Operational event log |
| Log invocation start / stop | An event each time a script block starts and stops (events 4105 and 4106) | PowerShell Operational event log |
| Module Logging | Pipeline execution details for the modules you specify (event 4103) | PowerShell Operational event log |
| Transcription | A text record of each session’s input and output | A file per session, in a folder you choose |
The first three all write to the same Microsoft-Windows-PowerShell/Operational event log. That detail matters later, because it means they compete for the same space.
These settings apply to Windows PowerShell 5.1. PowerShell 7 is a separate product with its own settings, which I’ll come back to.
Script Block Logging: the one worth having
If you only turn on one setting, make it this one. Event 4104 records the actual code PowerShell runs, which is what an investigator needs.
Its real strength is that it logs code after deobfuscation. An attacker can encode or chop up a command to hide it from a quick glance, but by the time PowerShell executes it, the engine sees the real code, and that’s what lands in the log. Very large script blocks are split across several 4104 events, so collection tools need to reassemble them.
It’s also efficient. Each unique script block is generally logged once, not every time it runs, so a loop that executes the same block ten thousand times doesn’t produce ten thousand events.
Even with it switched off, Windows PowerShell 5.1 logs script blocks that contain known suspicious keywords as warning events. That’s a useful safety net, but it isn’t a substitute for turning the setting on.
Invocation start and stop: volume without value
Script Block Logging has a checkbox to Log script block invocation start / stop events. It sounds like extra detail. In practice, it’s mostly extra noise.
With it on, PowerShell writes event 4105 when a script block starts and 4106 when it finishes. Unlike 4104, these are written on every invocation, including PowerShell’s own internal script blocks. A single script can generate thousands of them.
They contain no code, just the fact that something started and stopped. You already have the code from 4104. For almost every environment, I’d leave this checkbox off.
Module Logging: a performance cost and a data problem
Module Logging records pipeline execution details for the modules you list, written as event 4103. The common configuration is a module name of *, meaning every module. That’s where the costs start.
It overlaps with Script Block Logging. Much of what 4103 captures, you can already see from the code in 4104. The extra detail is in the parameter values and pipeline output, which is also the problem.
It has a real performance cost. Module Logging records the objects flowing through the pipeline. On a quick interactive command, nobody notices. On a script processing tens of thousands of objects, such as bulk Graph, Exchange Online or SQL work, the overhead is significant. The people who feel it are your heaviest scripters, and they’ll notice before you do.
It puts data in the log. Because it records pipeline contents, whatever flows through a pipeline can end up in the event log. That includes user records pulled from Graph, rows from a database, connection strings and credentials. More on that below.
If you need Module Logging, scope it to specific modules you care about rather than *. Otherwise, I’d leave it off and rely on Script Block Logging.
Transcription: thousands of files and nobody tidying up
Transcription writes a text file for every PowerShell session, recording what was typed and what came back. It’s readable and useful, but it has three problems that are easy to miss.
Volume. Every PowerShell host that starts creates a transcript, including the short-lived ones you never see: Intune detection and remediation scripts, installer wrappers such as PSADT, and monitoring agents. A single device can build up thousands of small files, and nothing deletes them.
Permissions. If transcripts go to a local folder, check who can read and write it. Users who can read the folder can read other people’s sessions. Users who can write to it, including an attacker, can delete or edit their own transcripts. The better pattern is a central share where devices can write but not read or modify existing files.
Retention. Nobody owns clean-up by default. Decide how long transcripts are kept and automate deleting them, or they’ll grow until someone notices a full disk.
If you use transcription, enable invocation headers, which timestamp each command, and send it to a locked-down central location rather than the local disk.
Log rollover: the silent evidence killer
This is the cost that hurts most, because you only find out about it during an incident.
The PowerShell Operational log has a fixed maximum size, and when it’s full, Windows overwrites the oldest events. The default is small, commonly around 15 MB. With Script Block Logging alone, that may hold weeks of history. Add invocation start and stop events and Module Logging for every module, and a busy device can roll that log in hours.
The result is perverse. By turning on more logging, you can end up with less usable history. The events you need from last Tuesday have already been overwritten by thousands of 4105 events from this morning’s installer.
Check the current size on a device:
wevtutil gl "Microsoft-Windows-PowerShell/Operational"The maxSize value is in bytes. To raise it, for example to 256 MB:
wevtutil sl "Microsoft-Windows-PowerShell/Operational" /ms:268435456Pick a size based on how quickly your devices fill it, not a round number. Watch a few typical devices for a week and work out how many days the log holds. The PowerShell Operational log isn’t one of the classic logs that Group Policy’s Event Log settings cover, so deploy the size change with a script, for example through an Intune remediation or your device build, and re-check it after major Windows updates.
Even a bigger log is only a buffer, though. The real answer to rollover is getting the events off the device, covered below.
Sensitive data in the logs
Logging captures what PowerShell sees, and PowerShell sees a lot. Script Block Logging records the code, so a script with a hard-coded password, API key or connection string puts that secret straight into the event log. Module Logging and transcription go further, recording parameter values and output, which can include personal data from Graph or SQL.
That has two consequences. First, your logs become a target. Anyone who can read the PowerShell Operational log, or your transcript folder, can harvest secrets from it. Second, when you forward logs to a SIEM, those secrets and that personal data go with them. That’s worth thinking about under your privacy obligations, not just security.
Three things help:
- Keep secrets out of scripts. Use managed identities, certificate-based authentication or a secrets vault instead of hard-coded credentials. Logging will expose a hard-coded secret sooner or later.
- Limit who can read the logs. Treat the event log and transcript location as sensitive.
- Consider Protected Event Logging. Windows can encrypt the contents of PowerShell events using a public key certificate, so only whoever holds the private key, typically your SIEM or security team, can read them. It adds certificate management overhead, but it stops anyone who can open Event Viewer from reading your scripts.
Microsoft makes the same point in its own documentation, recommending Protected Event Logging whenever Script Block Logging is used for anything other than diagnostics (Microsoft Learn: about_Logging).
The PowerShell 7 gap
This one catches a lot of people. The standard Group Policy and Intune Administrative Templates settings configure Windows PowerShell 5.1 only. PowerShell 7 (pwsh) is a separate product. It reads its own policy settings and writes to its own event log. If you’ve only configured the Windows PowerShell settings, anyone running pwsh can be completely unlogged.
To close the gap:
- Deploy the PowerShell 7 policy settings. PowerShell 7 ships its own ADMX templates, installed by running
InstallPSCorePolicyDefinitions.ps1from the PowerShell 7 install folder. Its settings include an option to use the Windows PowerShell policy values, so you can keep one set of settings in sync. In Intune, import the ADMX or check whether the Settings Catalog covers the PowerShell 7 settings you need. - Make sure the event provider is registered. On Windows, PowerShell 7’s event provider must be registered before it can write events, using
$PSHOME\RegisterManifest.ps1from an elevated prompt (Microsoft Learn: about_Logging_Windows). - Collect the right log. PowerShell 7 events land in the PowerShellCore/Operational log, not the Windows PowerShell one. Your SIEM collection needs both.
The quickest test is to run a harmless command in pwsh on a managed device, then check whether a matching 4104 event appears in PowerShellCore/Operational. If it doesn’t, PowerShell 7 activity on that device is invisible to you.
SIEM forwarding: logs only matter if they leave the device
If PowerShell events only exist on the device, they only exist for as long as the device is trustworthy. An attacker with admin rights can clear the event log, and a log that rolls over loses history anyway. Logs that never leave the device are a weak control.
Forwarding the events to a SIEM such as Microsoft Sentinel fixes both problems. With the Azure Monitor Agent, a data collection rule can use XPath queries to collect just the events you want:
Microsoft-Windows-PowerShell/Operational!*[System[(EventID=4104)]]
PowerShellCore/Operational!*[System[(EventID=4104)]]Add event 4103 if you’ve kept Module Logging for specific modules. Windows Event Forwarding to a collector is the traditional alternative if you’re not using an agent.
This is also where the most literal hidden cost appears. SIEMs usually charge by the volume ingested. Forwarding the whole Operational log, including thousands of 4105 and 4106 events per device per day, can add real money to your bill while adding almost nothing to your detections. Filtering to the events you actually use keeps both the noise and the cost down.
Microsoft’s own Azure Monitor guidance makes the cost point directly: you’re charged for the data you collect, so use custom XPath queries to collect only the events you need, and test them first with Get-WinEvent -FilterXPath (Microsoft Learn: Collect Windows events with Azure Monitor Agent).
Finally, agree with whoever runs the SIEM what they’ll do with the data. Collecting 4104 is only half the job. Someone needs detection rules or hunting queries that actually use it, or you’re paying to store evidence nobody looks at.
What I’d land on
This is the configuration I’d recommend for most environments as a starting point:
| Setting | Recommendation | Why |
|---|---|---|
| Script Block Logging | On | The event with real forensic value, deobfuscated and efficient |
| Invocation start / stop | Off | Huge volume, almost no detection value |
| Module Logging | Off, or scoped to specific modules | Performance cost, overlap with 4104, and data in the log |
| Transcription | On, with invocation headers, to a locked-down central share | Readable history without local tampering or clutter |
| Operational log size | Raised, based on measured fill rate | Stops rollover destroying history |
| PowerShell 7 | Configured separately, provider registered | Closes the pwsh blind spot |
| SIEM forwarding | 4104 from both logs, filtered | Evidence survives the device, at sensible cost |
My opinion after working through this: more logging isn’t automatically better logging. Every setting you enable has a cost in volume, performance, privacy or money, and the settings interact. A policy that turns everything on can leave you with a log that rolls in hours, a SIEM bill full of noise, secrets sitting in Event Viewer, and PowerShell 7 unlogged entirely.
The better approach is to start from the question an investigator will ask during an incident, which is usually “what code ran on this device?”, and configure just enough to answer it reliably.
Also check for conflicts before changing anything. Security baselines often enable Script Block Logging too, and if a baseline and an Administrative Templates profile both target the same devices, Intune will report a conflict. That’s how I found this configuration in the first place.
Checklist: PowerShell logging without the hidden costs
- Turn on Script Block Logging (event 4104).
- Leave invocation start and stop events (4105 and 4106) off.
- Turn Module Logging off, or scope it to specific modules rather than
*. - If you use transcription, enable invocation headers and write to a locked-down central share.
- Set a retention policy for transcripts.
- Measure how fast the PowerShell Operational log fills, and raise its size accordingly.
- Keep secrets out of scripts, and restrict who can read the logs.
- Consider Protected Event Logging for sensitive environments.
- Configure PowerShell 7 separately and register its event provider.
- Test that
pwshactivity appears in PowerShellCore/Operational. - Forward 4104 from both logs to your SIEM, filtered to what you use.
- Agree who builds detections on the data.
- Check for policy conflicts with security baselines.
Sources
- Microsoft Learn: about_Logging (Windows PowerShell 5.1)
- Microsoft Learn: about_Logging_Windows (PowerShell 7)
- Microsoft Learn: Collect Windows events with Azure Monitor Agent

From my early days on the helpdesk through roles as a service desk manager, systems administrator, and network engineer, I’ve spent more than 25 years in the IT world. As I transition into cyber security, my goal is to make tech a little less confusing by sharing what I’ve learned and helping others wherever I can.

