Enable PowerShell Script Block Logging and Constrained Language Mode
For developers and defenders who need better PowerShell visibility and safer execution on Windows endpoints. This walks you through turning on script block logging, verifying event output, and enforcing Constrained Language Mode with WDAC or AppLocker-backed policy so it actually sticks.
TL;DR — Turn on PowerShell script block logging with Group Policy or registry so executed code lands in the PowerShell Operational log, then enforce Constrained Language Mode with WDAC or AppLocker-backed policy if you want restrictions that users cannot bypass by just setting an environment variable. The most common failure is testing CLM with
$ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage', which only affects the current session and is not a security boundary. Reading time: ~5 min
Goal
When you finish, Windows endpoints will write PowerShell script block events to Microsoft-Windows-PowerShell/Operational, and PowerShell sessions on targeted machines will run in ConstrainedLanguage because of an application control policy, not because of a temporary shell setting.
Prerequisites
- Local Administrator on the Windows endpoint, or domain admin rights if using Group Policy
- Windows PowerShell 5.1 present — check with:
$PSVersionTable.PSVersion
- PowerShell event log access — check with:
Get-WinEvent -ListLog 'Microsoft-Windows-PowerShell/Operational'
- If enforcing CLM at scale: either AppLocker support or Windows Defender Application Control (WDAC) support on the target edition
- If using Group Policy: access to Group Policy Management on a domain-joined machine
- If using local policy only: ability to edit the registry and reboot or restart PowerShell
- A test script to execute, or use the sample in this article
Steps
Step 1: Confirm the PowerShell version and current language mode
Run:
$PSVersionTable.PSVersion
$ExecutionContext.SessionState.LanguageMode
You should see PowerShell 5.1.x or later and FullLanguage before enforcement.
Step 2: Enable script block logging with local registry settings
Run PowerShell as Administrator and apply these values:
New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -Force | Out-Null
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -Name 'EnableScriptBlockLogging' -Type DWord -Value 1
New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging' -Force | Out-Null
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging' -Name 'EnableModuleLogging' -Type DWord -Value 1
New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames' -Force | Out-Null
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames' -Name '*' -Type String -Value '*'
You should get no error output; the commands return to the prompt.
Step 3: If you use Group Policy, set the equivalent policy path
In Group Policy Management Editor, set these exact paths:
Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on PowerShell Script Block Logging → EnabledComputer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on Module Logging → Enabled- In
Module Names, add*
Then refresh policy on the endpoint:
gpupdate /force
You should see output shaped like:
Updating policy...
Computer Policy update has completed successfully.
User Policy update has completed successfully.
Step 4: Generate a test script block and confirm events are written
Run this exact test:
$test = 'Write-Output "SBL test"; Get-Date | Out-String'
Invoke-Expression $test
Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' -MaxEvents 20 |
Where-Object { $_.Id -in 4103,4104 } |
Select-Object TimeCreated, Id, ProviderName, Message |
Format-List
You should see event 4104 for script block logging and possibly 4103 for module logging. The message body should contain Write-Output "SBL test".
Step 5: Test Constrained Language Mode in a non-persistent way first
Run this only to see the behavior difference:
$ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage'
$ExecutionContext.SessionState.LanguageMode
New-Object System.Net.WebClient
You should see ConstrainedLanguage, followed by an error shaped like:
New-Object : Cannot create type. Only core types are supported in this language mode.
At line:1 char:1
+ New-Object System.Net.WebClient
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (:) [New-Object], PSNotSupportedException
+ FullyQualifiedErrorId : CannotCreateTypeConstrainedLanguage,Microsoft.PowerShell.Commands.NewObjectCommand
Step 6: Enforce CLM with AppLocker-backed policy
⚠️ This can block legitimate admin and deployment scripts. Test on one endpoint or OU first.
On systems where AppLocker is available, start the Application Identity service and set it to automatic:
Set-Service -Name AppIDSvc -StartupType Automatic
Start-Service -Name AppIDSvc
Get-Service -Name AppIDSvc
You should see Status : Running.
Then create and enforce default AppLocker rules in Local Security Policy:
Local Security Policy → Application Control Policies → AppLocker- Right-click
Executable Rules→Create Default Rules - Right-click
Windows Installer Rules→Create Default Rules - Right-click
Script Rules→Create Default Rules - Click
AppLocker→Configure rule enforcement→ setExecutable rules,Windows Installer rules, andScript rulestoEnforced
Update policy:
gpupdate /force
Open a new PowerShell session and run:
$ExecutionContext.SessionState.LanguageMode
You should see ConstrainedLanguage in the new session if the policy is active.
Step 7: Enforce CLM with WDAC if you use application control
⚠️ WDAC can cause application launch failures and endpoint downtime if you deploy an over-restrictive policy. Use audit mode first.
If your environment uses WDAC, deploy a policy that enforces application control, then open a new PowerShell session and check:
$ExecutionContext.SessionState.LanguageMode
You should see ConstrainedLanguage. In audit-first rollouts, verify the WDAC policy is present before switching to enforced mode.
Step 8: Capture a before/after test that defenders can reuse
Run these commands in a fresh PowerShell session after policy enforcement:
$ExecutionContext.SessionState.LanguageMode
[System.IO.File]::ReadAllText('C:\Windows\win.ini')
New-Object System.Net.WebClient
Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' -MaxEvents 10 |
Select-Object TimeCreated, Id, Message |
Format-List
You should see ConstrainedLanguage, core .NET type access may still work for allowed core types, New-Object System.Net.WebClient should fail, and recent 4104 events should exist.
Verify it works
Run all of the following on a targeted endpoint:
$ExecutionContext.SessionState.LanguageMode
Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' -MaxEvents 5 |
Select-Object TimeCreated, Id, Message |
Format-List
powershell.exe -NoProfile -Command "$ExecutionContext.SessionState.LanguageMode"
Expected results:
- The first command returns:
ConstrainedLanguage
- The event log query returns at least one recent
Id : 4104entry with the script content inMessage - Launching a new
powershell.exeprocess also returnsConstrainedLanguage; if only the current shell shows it, you did not enforce it with policy
Common pitfalls
Setting LanguageMode manually and thinking it is enforced
Mistake: using $ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage' as the deployment method.
Symptom: the current shell is restricted, but a new powershell.exe window returns FullLanguage.
Fix: deploy AppLocker or WDAC-backed application control so new sessions inherit ConstrainedLanguage automatically.
Enabling logging in the wrong registry path
Mistake: writing values under a non-policy path or misspelling ScriptBlockLogging.
Symptom: commands run fine, but Get-WinEvent shows no 4104 events.
Fix: use HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging with EnableScriptBlockLogging=1 exactly.
Checking the wrong event log
Mistake: looking only in Windows PowerShell classic logs or System.
Symptom: you conclude logging is broken even though events exist.
Fix: query Microsoft-Windows-PowerShell/Operational and filter for event IDs 4103 and 4104.
Forgetting to restart the PowerShell process after policy changes
Mistake: applying Group Policy or AppLocker rules, then testing in the same shell.
Symptom: LanguageMode still shows FullLanguage until much later.
Fix: close the session and start a new powershell.exe after gpupdate /force.
AppIDSvc is disabled
Mistake: creating AppLocker rules but leaving the Application Identity service stopped.
Symptom: rules do not apply, and PowerShell stays in FullLanguage.
Fix: run Set-Service -Name AppIDSvc -StartupType Automatic and Start-Service -Name AppIDSvc, then reopen PowerShell.
Assuming PowerShell 7 uses the same logging path and policy behavior as Windows PowerShell 5.1
Mistake: validating only in pwsh and expecting identical eventing and enforcement behavior.
Symptom: logs or restrictions do not match what you expected from Windows PowerShell.
Fix: validate first with powershell.exe 5.1 for script block logging and CLM enforcement, then test pwsh separately in your environment.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI