If you write PowerShell scripts, whether for automation, system administration, or any other purpose, writing professional code means more than just getting it to work. It means validating your code, documenting it properly, ensuring compatibility across environments, and following standards that make your scripts maintainable and portable.
In this blog post, I will cover five essential aspects of professional PowerShell development:
- Structuring and documenting your code - How to organize your scripts with regions, use comment-based help, write effective comments, and leverage code snippets.
- Documentation and versioning - How to create script header comments that provide metadata, version history, requirements, and links for your scripts.
- Code validation with PSScriptAnalyzer - How to catch errors, enforce best practices, and ensure your scripts are secure using the industry-standard static code analyzer.
- Constrained Language Mode compatibility - How to write scripts that work reliably in enterprise environments with strict security policies like WDAC and AppLocker.
- File encoding and line endings - How to configure your files for cross-platform compatibility and seamless version control.
By the end of this post, you will have the knowledge to produce PowerShell code that is not only functional but well-documented, secure, compatible, and ready for any environment.
Why you should validate your PowerShell code
Before diving into the how-to, let me explain why validating your PowerShell code is important:
Catch errors early. PSScriptAnalyzer can detect issues like undefined variables, missing cmdlet parameters, or incorrect syntax before you run your script. This saves time and prevents unexpected failures in production.
Follow best practices. The tool enforces coding standards and best practices, making your code more readable, maintainable, and consistent.
Improve security. PSScriptAnalyzer includes security rules that help identify potential vulnerabilities, such as using plain text passwords or invoking expressions dynamically.
Ensure compatibility. Some rules check for compatibility issues across different PowerShell versions and platforms, helping you write portable scripts.
Professionalism. Clean, validated code demonstrates professionalism and attention to detail, which is especially important when sharing code with colleagues or the community.
Structuring and documenting your code
Well-structured and well-documented code is easier to understand, maintain, and debug. This section covers how to organize your scripts using regions, document your code with comment-based help and inline comments, and leverage Visual Studio Code snippets to scaffold common patterns quickly.
Using regions to organize code
PowerShell supports #region and #endregion directives to create collapsible sections in your code. This is a PowerShell feature (not just a Visual Studio Code feature) that works in Visual Studio Code, PowerShell ISE, and other editors that support code folding.
Regions are particularly useful for organizing large scripts:
#region Parameters and Variables
$LogPath = "C:\Logs\Script.log"
$MaxRetries = 3
$Timeout = 30
#endregion
#region Helper Functions
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$timestamp - $Message" | Out-File -FilePath $LogPath -Append
}
function Test-Connection {
param([string]$Target)
# Connection test logic
}
#endregion
#region Main Script Logic
Write-Log "Script started"
# Main processing code here
Write-Log "Script completed"
#endregionUsing regions to organize PowerShell code
In Visual Studio Code, you can collapse regions by clicking the arrow next to the #region line, or use keyboard shortcuts:
| Action | Primary shortcut | Alternative shortcut |
|---|---|---|
| Fold region | Ctrl + Shift + [ | Ctrl + K, Ctrl + [ |
| Unfold region | Ctrl + Shift + ] | Ctrl + K, Ctrl + ] |
| Fold all regions | Ctrl + K, Ctrl + 0 | |
| Unfold all regions | Ctrl + K, Ctrl + J |
Note
On non-US keyboard layouts (such as Danish, German, or other Nordic keyboards), the [ and ] keys often require AltGr or other modifier combinations, which can conflict with the primary shortcuts. If the primary shortcuts do not work, try the alternative shortcuts or customize your keybindings in Visual Studio Code via File > Preferences > Keyboard Shortcuts.
For a complete list of keyboard shortcuts, see the Visual Studio Code keyboard shortcuts reference .
Visual Studio Code snippets for PowerShell
Visual Studio Code with the PowerShell extension includes many built-in snippets that help you quickly scaffold common code patterns. Type the snippet prefix and press Tab to expand.
| Snippet | Description |
|---|---|
comment-help | Complete comment-based help template |
region | #region / #endregion block |
| Snippet | Description |
|---|---|
function | Basic function with param block |
function-advanced | Advanced function with [CmdletBinding()] and full parameter attributes |
function-inline | Inline function definition |
To discover all available snippets, press Ctrl + Space in a PowerShell file to open the IntelliSense suggestions, or type a partial snippet name and browse the list.
You can also create custom snippets in Visual Studio Code for your own templates. Go to File > Preferences > Configure User Snippets > powershell.json and add your templates.
Script header comments
For standalone scripts (not functions), include a header comment block at the top of the file. This serves multiple purposes:
- Enables Get-Help support - Users can run
Get-Help .\YourScript.ps1 -Fullto see the synopsis, description, and examples, just like with cmdlets. - Self-documenting - Anyone opening the script immediately understands its purpose, requirements, and history.
- Maintenance tracking - The
.NOTESsection provides a natural place for version history and change logs. - Requirements documentation - Clearly states prerequisites like PowerShell version, modules, or permissions needed.
Here is an example of a script header:
<#
.SYNOPSIS
Daily backup script for user profile data.
.DESCRIPTION
This script creates compressed backups of user profile folders and stores
them on the backup server. It runs daily via scheduled task and maintains
the last 30 days of backups.
.NOTES
File Name : Backup-UserProfiles.ps1
Author : Your Name
Created : 2025-01-01
Modified : 2025-06-15
Version : 2.1
Requirements:
- PowerShell 5.1 or later
- Write access to \\backup\profiles$
- Run as scheduled task with appropriate permissions
Change Log:
v2.1 - Added email notification on failure
v2.0 - Switched to 7-Zip compression for better ratios
v1.0 - Initial release
.LINK
https://internal-wiki/backup-procedures
#>
#Requires -Version 5.1
#Requires -RunAsAdministrator
# Script code begins here...Script header comments
Important
Notice that the#Requires statements are placed after the comment-based help block, not at the very top of the file. This placement is intentional. If you place #Requires statements before the comment-based help block, PowerShell’s help system will not recognize the help content, and Get-Help will not work for your script. The comment-based help block must be the first non-blank, non-comment element in the script for Get-Help to detect it properly.Comment-based help for functions
PowerShell supports a special comment syntax that provides built-in help for your functions. When you add comment-based help, users can run Get-Help on your function just like they would for any built-in cmdlet. PSScriptAnalyzer’s PSProvideCommentHelp rule encourages this practice.
Here is a template for comment-based help:
function Get-SystemInfo {
<#
.SYNOPSIS
Retrieves basic system information from a computer.
.DESCRIPTION
The Get-SystemInfo function collects and returns basic system information
including computer name, operating system, and memory details. This function
can be used for inventory or troubleshooting purposes.
.PARAMETER ComputerName
The name of the computer to query. Defaults to the local computer.
.PARAMETER IncludeMemory
If specified, includes detailed memory information in the output.
.EXAMPLE
Get-SystemInfo
Returns system information for the local computer.
.EXAMPLE
Get-SystemInfo -ComputerName "SERVER01" -IncludeMemory
Returns system information including memory details for SERVER01.
.INPUTS
System.String
You can pipe computer names to this function.
.OUTPUTS
PSCustomObject
Returns a custom object with system information properties.
.NOTES
Author: Your Name
Version: 1.0
Date: 2025-01-01
.LINK
https://your-documentation-url.com
#>
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[string]$ComputerName = $env:COMPUTERNAME,
[switch]$IncludeMemory
)
# Function implementation here
}Example of comment-based help in PowerShell
The most commonly used comment-based help keywords are:
| Keyword | Purpose |
|---|---|
.SYNOPSIS | A brief description of the function (one line) |
.DESCRIPTION | A detailed description of what the function does |
.PARAMETER | Description of a specific parameter (repeat for each) |
.EXAMPLE | Usage examples (repeat for multiple examples) |
.INPUTS | Types of objects that can be piped to the function |
.OUTPUTS | Types of objects the function returns |
.NOTES | Additional information like author, version, or caveats |
.LINK | Links to related documentation or functions |
Best practices for inline comments
Inline comments explain specific lines or blocks of code. Here are some guidelines for effective commenting:
Explain why, not what. The code already shows what is happening. Comments should explain why you made a particular decision:
# Bad: This comment just repeats the code
# Set the timeout to 30
$Timeout = 30
# Good: This comment explains the reasoning
# 30 seconds allows for slow network connections without blocking too long
$Timeout = 30Effective inline comments
Comment complex logic. When code is not immediately obvious, add a comment:
# Use bitwise AND to check if the third bit is set (indicates admin rights)
if ($flags -band 0x4) {
# User has administrative privileges
}Commenting complex logic
Mark workarounds and technical debt. When you implement a workaround, document it:
# WORKAROUND: The API returns dates in a non-standard format.
# Remove this conversion when API v2 is released (planned Q2 2025).
$date = [datetime]::ParseExact($apiDate, "dd-MM-yyyy", $null)Documenting workarounds
Use TODO comments for future work. Many editors highlight TODO comments:
# TODO: Add error handling for network timeouts
# TODO: Implement retry logic for transient failures
# FIXME: This breaks when the path contains special charactersUsing TODO comments
Avoid obvious comments. Do not comment code that is self-explanatory:
# Bad: Obvious and adds no value
# Get the current date
$today = Get-Date
# Also bad: Comment lies or is outdated
# Get yesterday's date
$today = Get-Date # This is actually today, not yesterday!Avoiding obvious comments
Keeping comments up to date
Outdated comments are worse than no comments at all. When you modify code, always review and update related comments. A comment that contradicts the code will confuse anyone reading it, including your future self.
Consider using a linting process or code review checklist that specifically checks for:
- Comments that no longer match the code
- TODO items that have been completed but not removed
- Workaround comments for issues that have been fixed upstream
Using AI to help update comments
AI coding assistants like GitHub Copilot, ChatGPT, or Claude can help identify outdated comments and suggest updates. When you modify code, you can ask the AI to review whether the associated comments still accurately describe the behavior.
Review this function and its comments. Are the inline comments and comment-based help still accurate after the recent changes? Suggest updates where needed.Using AI to review comments
This approach is especially useful after refactoring or when inheriting code from others. You can also ask the AI to generate comment-based help for functions that lack documentation, or to improve existing descriptions.
Note
Always review AI-generated suggestions carefully before applying them. The AI may not fully understand the intent behind your code, the broader context of your project, or organization-specific conventions. Use AI as a helpful assistant, not as a replacement for your own judgment.Using PSScriptAnalyzer in Visual Studio Code
If you use Visual Studio Code for PowerShell development (which I highly recommend), you get PSScriptAnalyzer integration out of the box. Visual Studio Code monitors your PowerShell code as you write it, providing real-time feedback on errors, warnings, and best practice violations. This means you can catch and fix issues immediately, rather than discovering them later when running your scripts.
For most developers, using PSScriptAnalyzer through Visual Studio Code is the recommended approach. It provides immediate feedback as you type and integrates seamlessly into your development workflow.
Important
Update:Invoke-ScriptAnalyzer from the terminal, in tasks, or in CI/CD pipelines, you must install PSScriptAnalyzer as a standalone module. See How to set up PSScriptAnalyzer beyond the editor for installation steps and update strategies.Install the PowerShell extension
First, ensure you have the PowerShell extension installed in Visual Studio Code. This extension provides rich PowerShell language support, including IntelliSense, debugging, and real-time code analysis powered by an internal copy of PSScriptAnalyzer. The extension handles editor linting automatically - squiggly underlines, Problems panel entries, and quick fixes work without any additional setup. However, if you need to run Invoke-ScriptAnalyzer from the terminal or in tasks, you must install PSScriptAnalyzer as a standalone module.
To install the extension:
- Open Visual Studio Code.
- Press
Ctrl + Shift + Xto open the Extensions view. - Search for “PowerShell” and install the extension by Microsoft.
Once installed, PSScriptAnalyzer starts working immediately - no additional configuration required.
Real-time code monitoring
Once the PowerShell extension is installed, Visual Studio Code continuously monitors your PowerShell code as you write it. Every time you type, save, or open a PowerShell file, PSScriptAnalyzer automatically runs in the background and analyzes your code. Issues detected by PSScriptAnalyzer appear as:
- Squiggly underlines in the editor (yellow for warnings, red for errors).
- Entries in the Problems panel (
Ctrl + Shift + M). - Hover information - hover over an underlined issue to see the rule name and description.
- Quick fixes - some issues offer automatic fixes via the lightbulb icon or
Ctrl + ..
This real-time feedback loop means you are always aware of potential issues in your code. You do not need to manually run PSScriptAnalyzer or wait until you execute your script to discover problems. Visual Studio Code acts as your coding assistant, guiding you toward better PowerShell code as you write it.
Verify PSScriptAnalyzer is enabled in Visual Studio Code
PSScriptAnalyzer should be enabled by default, but it is good practice to verify that it is active, especially if you are not seeing any analysis feedback. Here is how to check:
- Press
Ctrl + ,to open the Settings. - In the search bar, type
powershell script analysis. - Look for the setting PowerShell > Script Analysis: Enable.
- Ensure the checkbox is checked (enabled).
Alternatively, you can verify via the Command Palette:
- Press
F1orCtrl + Shift + Pto open the Command Palette. - Type
PowerShell: Show Session Menuand press Enter. - Check that the PowerShell extension is running (you should see version information).
If script analysis is not working, try:
- Reloading the window (
Ctrl + Shift + P>Developer: Reload Window). - Checking the PowerShell extension output (
View>Output> selectPowerShell Extension Logs). - Ensuring no workspace settings are overriding user settings.
Configure PSScriptAnalyzer settings
You can customize PSScriptAnalyzer behavior in Visual Studio Code by modifying your settings. Press Ctrl + , to open Settings, then search for “script analysis”.
Key settings include:
powershell.scriptAnalysis.enable: Enable or disable script analysis (default: true).
powershell.scriptAnalysis.settingsPath: Path to a PSScriptAnalyzer settings file for custom rule configurations.
powershell.codeFormatting.preset: Choose a formatting preset (OTBS, Stroustrup, Allman, or Custom).
You can also configure these settings in your settings.json:
{
"powershell.scriptAnalysis.enable": true,
"powershell.scriptAnalysis.settingsPath": "C:\\Scripts\\PSScriptAnalyzerSettings.psd1"
}Create a custom settings file
For advanced configurations, you can create a PSScriptAnalyzer settings file (.psd1) to customize which rules to include or exclude:
# PSScriptAnalyzerSettings.psd1
@{
Severity = @('Error', 'Warning')
ExcludeRules = @(
'PSAvoidUsingWriteHost'
)
Rules = @{
PSUseCompatibleSyntax = @{
Enable = $true
TargetVersions = @('5.1', '7.0', '7.4')
}
}
}Example PSScriptAnalyzer settings file
This settings file:
- Shows only errors and warnings (not information).
- Excludes the
PSAvoidUsingWriteHostrule. - Enables compatibility checking for PowerShell 5.1, 7.0, and 7.4.
Using PSScriptAnalyzer from the command line
While Visual Studio Code includes PSScriptAnalyzer automatically, there are scenarios where you need to run PSScriptAnalyzer from the command line:
- CI/CD pipelines - Running automated code analysis in GitHub Actions, Azure DevOps, or other build systems.
- Servers without Visual Studio Code - Validating scripts on production servers or jump hosts.
- Automated scripts - Building custom validation workflows or pre-commit hooks.
- PowerShell ISE users - If you prefer PowerShell ISE over Visual Studio Code.
Prerequisites: Installing PSScriptAnalyzer
Unlike Visual Studio Code (where PSScriptAnalyzer is bundled with the PowerShell extension), command-line usage requires installing the module separately. PSScriptAnalyzer is available from the PowerShell Gallery and can be installed with a single command.
To install PSScriptAnalyzer, open a PowerShell terminal and run:
Install-Module -Name PSScriptAnalyzer -Repository PSGallery -ForceTo verify the installation, run:
Get-Module -Name PSScriptAnalyzer -ListAvailableYou should see output similar to:
ModuleType Version PreRelease Name PSEdition ExportedCommands
---------- ------- ---------- ---- --------- ----------------
Script 1.23.0 PSScriptAnalyzer Desktop {Get-ScriptAnalyzerRule, Invoke-Forma...Verifying PSScriptAnalyzer installation
Note that if you use both Visual Studio Code and the standalone module, they operate independently. Visual Studio Code uses its bundled version, while command-line usage relies on the installed module.
Running Invoke-ScriptAnalyzer
The primary cmdlet for analyzing scripts is Invoke-ScriptAnalyzer. Here are some examples of how to use it:
Tip
These commands work great in Visual Studio Code’s integrated terminal too! Open the terminal (Ctrl+ ` or View → Terminal), navigate to your project folder, and run the commands directly - no need to leave your editor.Analyze a single script
To analyze a single PowerShell script file:
Invoke-ScriptAnalyzer -Path "C:\Scripts\MyScript.ps1"This command analyzes the specified script and outputs any warnings or errors found.
Analyze a folder of scripts
To analyze all PowerShell scripts in a folder:
Invoke-ScriptAnalyzer -Path "C:\Scripts" -RecurseThe -Recurse parameter ensures that all scripts in subfolders are also analyzed.
Include specific rules
If you want to check only specific rules, use the -IncludeRule parameter:
Invoke-ScriptAnalyzer -Path "C:\Scripts\MyScript.ps1" -IncludeRule PSAvoidUsingPlainTextForPassword, PSAvoidUsingConvertToSecureStringWithPlainTextExclude specific rules
Conversely, if you want to exclude certain rules:
Invoke-ScriptAnalyzer -Path "C:\Scripts\MyScript.ps1" -ExcludeRule PSAvoidUsingWriteHostAnalyze with a specific severity
To show only errors or warnings of a certain severity:
Invoke-ScriptAnalyzer -Path "C:\Scripts\MyScript.ps1" -Severity Error, WarningAvailable severity levels are: Error, Warning, Information, and ParseError.
Example output
When PSScriptAnalyzer finds issues, the output looks like this:
RuleName Severity ScriptName Line Message
----------- ---------- -------------- ------ ---------
PSAvoidUsingWriteHost Warning MyScript.ps1 15 Avoid using Write-Host because it might not work in all hosts...
PSUseDeclaredVarsMoreThanAssignments Warning MyScript.ps1 23 Variable 'unused' is assigned but never used.
PSAvoidUsingPlainTextForPassword Warning MyScript.ps1 42 Parameter 'Password' should use SecureString...Sample PSScriptAnalyzer output
Understanding PSScriptAnalyzer rules and recommendations
PSScriptAnalyzer rules are organized into categories that help you understand the type of issue being flagged. Understanding these categories helps you prioritize which issues to address first.
Rule categories
Security rules - These rules identify potential security vulnerabilities in your scripts, such as using plain text passwords, hardcoded credentials, or dangerous cmdlets like Invoke-Expression. Security rules should always be addressed.
Code quality rules - These rules enforce best practices that improve code readability and maintainability, such as using approved verbs, providing comment-based help, and avoiding aliases.
Compatibility rules - These rules check for syntax and cmdlets that may not work across different PowerShell versions or platforms. Essential for scripts that need to run in mixed environments.
Performance rules - These rules identify patterns that could slow down script execution or consume unnecessary resources.
Common rules you should know
PSScriptAnalyzer includes many rules. Here are some of the most important ones:
| Rule Name | Severity | Description |
|---|---|---|
PSAvoidUsingPlainTextForPassword | Warning | Avoid using plain text passwords |
PSAvoidUsingConvertToSecureStringWithPlainText | Warning | Avoid converting plain text to SecureString |
PSAvoidUsingInvokeExpression | Warning | Avoid using Invoke-Expression |
PSUseShouldProcessForStateChangingFunctions | Warning | Use ShouldProcess for functions that change state |
PSAvoidUsingWriteHost | Warning | Avoid Write-Host, use Write-Output instead |
PSUseDeclaredVarsMoreThanAssignments | Warning | Variables should be used after assignment |
PSAvoidUsingCmdletAliases | Warning | Use full cmdlet names instead of aliases |
PSProvideCommentHelp | Information | Provide comment-based help for functions |
PSUseApprovedVerbs | Warning | Use approved PowerShell verbs for function names |
PSReviewUnusedParameter | Warning | Review parameters that are defined but not used |
PSUseSingularNouns | Warning | Function names should use singular nouns |
PSAvoidGlobalVars | Warning | Avoid using global variables |
Viewing all available rules
To see all available PSScriptAnalyzer rules, run:
Get-ScriptAnalyzerRule | Select-Object RuleName, Severity, DescriptionTo view rules by severity:
Get-ScriptAnalyzerRule | Group-Object Severity | Select-Object Name, CountTo find rules related to a specific topic (e.g., security):
Get-ScriptAnalyzerRule | Where-Object { $_.RuleName -like "*Security*" -or $_.Description -like "*security*" }Best practices and recommendations
When working with PSScriptAnalyzer, consider these recommendations:
Run it regularly. Make PSScriptAnalyzer part of your development workflow. Run it before committing code or as part of your CI/CD pipeline.
Start with default rules. The default rule set covers the most important best practices. Start with these before adding custom rules.
Always address errors first. Errors indicate serious issues that could cause your script to fail or behave unexpectedly.
Take warnings seriously. Warnings often highlight best practice violations that could lead to maintenance issues or security vulnerabilities down the road.
Do not ignore information messages. While less critical, these messages often suggest improvements that make your code more professional and easier to understand.
Review security rules carefully. Any rule flagged as a security issue should be carefully reviewed, even if you think it is a false positive.
Use suppression sparingly. You can suppress specific warnings using [Diagnostics.CodeAnalysis.SuppressMessageAttribute()], but only do this when you have a good reason and document why:
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
param()
Write-Host "This is intentional"Suppressing a PSScriptAnalyzer warning
Keep PSScriptAnalyzer updated. New rules are added regularly. How you update PSScriptAnalyzer depends on how you use it:
Visual Studio Code users: The bundled PSScriptAnalyzer is updated when the PowerShell extension is updated. To update the extension, go to the Extensions view (
Ctrl + Shift + X), find the PowerShell extension, and click Update if available.Command line users: If you installed PSScriptAnalyzer separately via
Install-Module, update it manually withUpdate-Module -Name PSScriptAnalyzer.
Share your settings. If you work in a team, share your PSScriptAnalyzer settings file in your repository to ensure consistent code quality across the team.
For a complete list of rules and detailed documentation, visit the PSScriptAnalyzer rules documentation .
PowerShell Constrained Language Mode
In enterprise environments, security policies like Application Control for Business (formerly Windows Defender Application Control, commonly known as WDAC) or AppLocker may enforce PowerShell Constrained Language Mode (CLM). Understanding CLM is essential for writing scripts that work reliably in secured environments.
What is Constrained Language Mode?
Constrained Language Mode (CLM) is a PowerShell security feature that restricts the language elements available to scripts. When CLM is active, PowerShell limits access to sensitive language features that could be exploited by malicious scripts. This is commonly enforced in organizations that use application control policies to protect their endpoints.
CLM is one of several language modes in PowerShell:
- FullLanguage - All language features are available (default mode).
- ConstrainedLanguage - Restricted access to .NET types, COM objects, and other sensitive features.
- RestrictedLanguage - Very limited mode, primarily for Data sections.
- NoLanguage - Only cmdlets and functions are allowed, no script language elements.
How to detect the current language mode
To check which language mode your PowerShell session is running in:
$ExecutionContext.SessionState.LanguageModeCheck current PowerShell language mode
This returns one of the language mode values. You can use this in your scripts to detect and handle CLM:
if ($ExecutionContext.SessionState.LanguageMode -eq 'ConstrainedLanguage') {
Write-Warning "Running in Constrained Language Mode. Some features may be unavailable."
}Detecting Constrained Language Mode in PowerShell
How to enable Constrained Language Mode for testing
To test whether your scripts are CLM-compatible, you can temporarily enable Constrained Language Mode in a PowerShell session. This allows you to verify your code works correctly before deploying it to secured environments.
Important: Once you switch to Constrained Language Mode, you cannot switch back to Full Language Mode in the same session. This is a security feature. You will need to close and reopen PowerShell to return to Full Language Mode.
To enable Constrained Language Mode in your current session:
$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"Enable Constrained Language Mode in PowerShell
After running this command, verify the mode has changed:
$ExecutionContext.SessionState.LanguageModeVerify PowerShell language mode
The output should show ConstrainedLanguage.
Now you can test your script in this restricted environment:
# Run your script to test CLM compatibility
.\MyScript.ps1If your script uses features that are blocked in CLM, you will see errors like:
Cannot invoke method. Method invocation is supported only on core types in this language mode.Example error in Constrained Language Mode
or
Cannot create type. Only core types are supported in this language mode.Example error in Constrained Language Mode
These errors indicate which parts of your script need to be rewritten using CLM-compatible alternatives.
Testing workflow recommendation:
- Open a new PowerShell window specifically for CLM testing.
- Enable Constrained Language Mode using the command above.
- Run your script and note any errors.
- Close the PowerShell window when done.
- Fix the issues in your regular PowerShell environment.
- Repeat until your script runs without errors in CLM.
For more thorough testing, consider setting up a test environment with actual WDAC or AppLocker policies. This simulates the real conditions your scripts will face in production and catches edge cases that manual CLM testing might miss.
What is restricted in Constrained Language Mode?
When CLM is active, the following features are restricted or unavailable:
No direct .NET type access. You cannot use [System.IO.File]::ReadAllText() or similar .NET method calls. Only a small set of approved types are allowed.
No COM objects. Creating COM objects with New-Object -ComObject is blocked.
No Add-Type. You cannot compile and load custom .NET code using Add-Type.
No custom classes. PowerShell 5+ class definitions are not allowed.
Limited type conversions. Many type accelerators and conversions are restricted.
No Invoke-Expression with untrusted input. Dynamic code execution is blocked.
Writing CLM-compatible scripts
To ensure your scripts work in Constrained Language Mode, follow these guidelines:
Use cmdlets instead of .NET methods. Replace .NET method calls with equivalent PowerShell cmdlets:
# Instead of this (blocked in CLM):
# [System.IO.File]::ReadAllText("C:\Scripts\config.txt")
# Use this (works in CLM):
Get-Content -Path "C:\Scripts\config.txt" -RawCLM-compatible file reading in PowerShell
Avoid Add-Type and custom classes. If you need custom functionality, consider using modules or functions instead of compiled .NET code.
Use approved type accelerators. Only certain type accelerators work in CLM. Stick to basic types like [string], [int], [bool], [array], and [hashtable].
Test in CLM. Before deploying scripts to secured environments, test them in Constrained Language Mode. You can simulate CLM for testing (though this requires specific conditions that are typically set by system policies).
Provide graceful fallbacks. When possible, detect CLM and provide alternative code paths:
function Get-CustomFileHash {
param([string]$Path)
if ($ExecutionContext.SessionState.LanguageMode -eq 'ConstrainedLanguage') {
# Use cmdlet approach (CLM-compatible)
Get-FileHash -Path $Path -Algorithm SHA256
} else {
# Use .NET approach (faster, but not CLM-compatible)
$stream = [System.IO.File]::OpenRead($Path)
try {
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$hash = $sha256.ComputeHash($stream)
[BitConverter]::ToString($hash) -replace '-', ''
} finally {
$stream.Close()
}
}
}CLM-compatible function example in PowerShell
Using AI to refactor for CLM compatibility
Refactoring .NET method calls to cmdlet equivalents can be tedious, especially for complex code. AI coding assistants can help with this conversion. If CLM testing reveals incompatible code, try a prompt like:
This PowerShell code uses .NET methods that do not work in Constrained Language Mode. Rewrite it using only built-in cmdlets and approved type accelerators.AI prompt for CLM refactoring
AI assistants are particularly good at suggesting cmdlet alternatives for common .NET patterns, such as file operations, string manipulation, and date handling. This can significantly speed up the process of making your scripts CLM-compatible.
Note
As with any AI-generated code, always test the refactored version in a CLM environment before deploying to production. The AI may suggest alternatives that work differently or have edge cases you need to handle.PSScriptAnalyzer compatibility rules
While PSScriptAnalyzer does not have rules specifically designed to detect CLM restrictions, its compatibility rules can help you write more portable scripts that are less likely to cause issues in constrained environments:
PSUseCompatibleTypes - Checks for types that may not be available in all PowerShell environments. Scripts that avoid uncommon types are generally easier to adapt for CLM.
PSUseCompatibleCommands - Verifies that cmdlets are available in target environments. Using standard cmdlets instead of .NET methods aligns with CLM best practices.
To enable these compatibility rules, add them to your PSScriptAnalyzer settings file:
# PSScriptAnalyzerSettings.psd1
@{
Rules = @{
PSUseCompatibleCommands = @{
Enable = $true
TargetProfiles = @(
'win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core'
'win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework'
)
}
PSUseCompatibleTypes = @{
Enable = $true
TargetProfiles = @(
'win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core'
'win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework'
)
}
}
}PSScriptAnalyzer settings for compatibility rules
When CLM matters
Consider CLM compatibility when:
- Your scripts will run on endpoints managed by Microsoft Intune with WDAC policies.
- Your organization uses AppLocker or similar application control solutions.
- You are developing scripts for enterprise deployment where security policies are strictly enforced.
- You are creating tools or modules for the broader community that may be used in secured environments.
By understanding and testing for Constrained Language Mode, you ensure your PowerShell scripts work reliably across all environments, including those with the strictest security policies.
File encoding and line endings
Beyond code analysis rules, the way you save your PowerShell files matters. File encoding and line endings can affect script compatibility, version control, and even script execution. Choosing the right settings ensures your scripts work consistently across different environments and tools.
Understanding file encoding
When you save a PowerShell script, the text is converted to bytes using a character encoding. The most common encodings for PowerShell scripts are:
- UTF-8 without BOM - The recommended encoding for modern PowerShell scripts.
- UTF-8 with BOM - UTF-8 with a
Byte Order Markat the beginning of the file. - UTF-16 (Unicode) - Used by older PowerShell and some Windows tools.
- ASCII - Limited to 128 characters, no support for special characters.
Why UTF-8 without BOM is the best choice
UTF-8 without BOM is the recommended encoding for PowerShell scripts, and here is why:
Cross-platform compatibility. PowerShell 7 and later run on Windows, macOS, and Linux. UTF-8 without BOM is the standard encoding on Unix-like systems. Scripts saved with BOM may cause issues on non-Windows platforms, especially when used as shell scripts or when the first line contains a shebang (#!/usr/bin/env pwsh).
Git and version control friendly. Git and other version control systems handle UTF-8 without BOM more gracefully. The BOM can appear as unexpected characters in diffs, cause merge conflicts, and create inconsistencies when team members use different editors.
Web and API compatibility. When PowerShell scripts interact with web services, REST APIs, or read/write JSON and XML files, UTF-8 without BOM is the expected encoding. The BOM can corrupt data or cause parsing errors in some scenarios.
Modern tooling standard. Most modern editors, linters, and CI/CD pipelines expect UTF-8 without BOM. Tools like PSScriptAnalyzer, GitHub Actions, and Azure DevOps work best with this encoding.
No hidden characters. The BOM adds three invisible bytes (EF BB BF) at the beginning of your file. These bytes can cause subtle issues, such as breaking scripts that are concatenated together or parsed by other tools.
PowerShell 7 default. PowerShell 7 and later default to UTF-8 without BOM for file operations, aligning with cross-platform best practices. Using the same encoding for your scripts ensures consistency.
When to use UTF-8 with BOM
There are a few scenarios where UTF-8 with BOM might still be appropriate:
- Legacy Windows PowerShell 5.1 scripts that must work with older Windows tools expecting BOM.
- Scripts opened in Notepad (older versions of Notepad needed BOM to detect UTF-8).
- Integration with legacy systems that specifically require BOM.
However, for new scripts and cross-platform development, UTF-8 without BOM is the clear choice.
Line endings: LF vs CRLF
Line endings are another important consideration. Different operating systems use different characters to mark the end of a line:
- LF (Line Feed,
\n) - Used by Linux, macOS, and Unix systems. - CRLF (Carriage Return + Line Feed,
\r\n) - Used by Windows. - CR (Carriage Return,
\r) - Used by old Mac systems (rarely seen today).
Why LF is the better choice for PowerShell scripts
While Windows traditionally uses CRLF, LF is increasingly the preferred choice for PowerShell scripts, especially for cross-platform and collaborative development:
Cross-platform compatibility. PowerShell 7 runs on Windows, macOS, and Linux. Scripts with LF line endings work correctly on all platforms, while CRLF can cause issues on Unix-like systems.
Git best practices. Git’s default behavior is to normalize line endings. Using LF consistently avoids messy diffs where the only change is line ending conversion. It also prevents the “no newline at end of file” warnings and whitespace noise in pull requests.
Consistency in repositories. When working in teams or contributing to open-source projects, LF is the standard. Mixing line endings causes confusion and unnecessary changes in version control history.
Modern editor support. Visual Studio Code, and most modern editors can work with LF on Windows without issues. PowerShell itself handles both LF and CRLF correctly when executing scripts.
Smaller file size. LF uses one byte per line ending, while CRLF uses two. For large scripts, this adds up.
Configuring Visual Studio Code for optimal file settings
Visual Studio Code makes it easy to configure encoding and line endings for PowerShell development. Add these settings to your settings.json to ensure consistency:
{
"[powershell]": {
"files.encoding": "utf8",
"files.eol": "\n"
}
}Visual Studio Code settings for PowerShell file encoding and line endings
These settings ensure:
- files.encoding:
utf8saves files as UTF-8 without BOM (Visual Studio Code’sutf8setting means without BOM;utf8bomwould add the BOM). - files.eol:
\nuses LF line endings for new files.
You can also set these as global defaults:
{
"files.encoding": "utf8",
"files.eol": "\n"
}Visual Studio Code global settings for file encoding and line endings
Using .editorconfig for team consistency
While Visual Studio Code settings work well for individual developers, they do not travel with your code. When you share a repository with teammates or open-source contributors, each person may have different editor settings, leading to inconsistent formatting, mixed line endings, and noisy diffs.
EditorConfig solves this problem. It is an open standard for defining coding styles that works across many editors and IDEs, including Visual Studio Code, Visual Studio, JetBrains IDEs, Sublime Text, and others. When you add an .editorconfig file to your repository, editors that support EditorConfig automatically apply those settings to files in that project, overriding user preferences for that workspace.
How it works:
- Create a file named
.editorconfig(note the leading dot) in the root of your repository. - The file uses a simple INI-style format with sections for different file types.
- When you open a file, your editor reads the
.editorconfigfile and applies the matching settings. - The
root = trueline tells the editor to stop searching for.editorconfigfiles in parent directories.
Visual Studio Code requires the EditorConfig for VS Code extension to support .editorconfig files. Once installed, the settings in .editorconfig override your user and workspace settings for matching files.
Here is an example .editorconfig for PowerShell projects:
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.ps1]
indent_style = space
indent_size = 4
[*.psd1]
indent_style = space
indent_size = 4
[*.psm1]
indent_style = space
indent_size = 4
[*.json]
indent_style = space
indent_size = 2Example .editorconfig for PowerShell projects
Key settings explained:
| Setting | Description |
|---|---|
root = true | Stops the editor from looking for .editorconfig files in parent directories |
charset = utf-8 | Sets file encoding to UTF-8 (without BOM) |
end_of_line = lf | Uses LF line endings |
insert_final_newline = true | Ensures files end with a newline character |
trim_trailing_whitespace = true | Removes trailing spaces when saving |
indent_style = space | Uses spaces instead of tabs for indentation |
indent_size = 4 | Sets indentation to 4 spaces (PowerShell community standard) |
The [*] section applies to all files, while [*.ps1] and similar sections apply only to files matching that pattern. This lets you define different settings for different file types in the same repository.
For more details on finding and removing trailing whitespace in Visual Studio Code, see my post How to remove trailing spaces in Visual Studio Code.
Configuring Git for line ending handling
While .editorconfig controls how your editor saves files, .gitattributes controls how Git handles files when committing and checking out. This ensures consistent line endings in your repository regardless of what operating system contributors use.
Create a file named .gitattributes (note the leading dot) in the root of your repository. Git reads this file automatically and applies the rules to all matching files.
my-project/
├── .git/ ← Git's internal folder (hidden in Visual Studio Code)
├── .editorconfig ← Editor configuration
├── .gitattributes ← Git attributes file goes here
├── .gitignore ← Git ignore file
├── PSScriptAnalyzerSettings.psd1 ← PSScriptAnalyzer settings
├── solution/
│ └── MyScript.ps1
└── README.mdRepository structure showing .gitattributes placement
Here is an example .gitattributes for PowerShell projects:
# Set default behavior to automatically normalize line endings
* text=auto eol=lf
# PowerShell files
*.ps1 text eol=lf
*.psd1 text eol=lf
*.psm1 text eol=lf
# Keep Windows batch files with CRLF
*.cmd text eol=crlf
*.bat text eol=crlfExample .gitattributes for PowerShell projects
This configuration tells Git to:
- Normalize all text files to LF in the repository.
- Keep PowerShell files with LF line endings.
- Preserve CRLF for Windows batch files that require it.
For more details on .gitattributes and line ending configuration, see Configuring Git to handle line endings .
Using .editorconfig and .gitattributes together
You might wonder whether you need both .editorconfig and .gitattributes, or if one is enough. The recommendation is to use both:
.editorconfigprevents problems at the source - it ensures files are saved with the correct encoding and line endings when you create or edit them..gitattributesacts as a safety net at the repository level - it normalizes files when committing, even if a contributor’s editor is not configured correctly.
Think of it this way: .editorconfig says “save files the right way,” while .gitattributes says “even if someone did not, fix it on commit.”
If you could use one only, .gitattributes is more critical because it enforces consistency for the entire repository regardless of individual editor settings. However, using both gives you the best experience - correct files from the start and repository-level protection as a fallback.
Using .vscode/extensions.json to recommend Visual Studio Code extensions to your team
The extensions.json file in Visual Studio Code is used to recommend extensions for a project, ensuring that all team members have a consistent development environment. When a team member opens the project folder, Visual Studio Code will prompt them to install the recommended extensions, reducing setup friction and ensuring everyone has the right tools.
This file should be placed in the .vscode folder at the root of your project:
my-powershell-project/
├── .vscode/
│ ├── extensions.json ← Extension recommendations
│ ├── settings.json ← Workspace settings
└── README.mdProject structure with extensions.json
Here is an example extensions.json for PowerShell projects:
{
"recommendations": [
"ms-vscode.powershell",
"editorconfig.editorconfig"
],
"unwantedRecommendations": []
}Example extensions.json for PowerShell projects
The recommendations array contains extension identifiers (in the format publisher.extension-name) that Visual Studio Code will suggest installing. The unwantedRecommendations array can be used to explicitly exclude extensions that should not be recommended for this project.
| Extension | Description |
|---|---|
ms-vscode.powershell | Official PowerShell extension with IntelliSense, debugging, and PSScriptAnalyzer integration |
editorconfig.editorconfig | Adds support for .editorconfig files |
To find an extension’s identifier, open the extension in Visual Studio Code’s Extensions view, click the gear icon, and select Copy Extension ID. Alternatively, you can find it on the Visual Studio Code Marketplace page for the extension.
When a team member opens the project, Visual Studio Code will display a notification suggesting they install the recommended extensions. They can also view all workspace recommendations by opening the Extensions view (Ctrl + Shift + X) and filtering by @recommended.
Tip
To share these recommendations with your team, make sure the .vscode folder is committed to your Git repository. While some developers exclude .vscode by default, the extensions.json and settings.json files are intended to be shared. You can selectively ignore personal settings by adding specific files to .gitignore rather than the entire folder.
If you include .vscode/settings.json with file formatting settings (encoding, line endings, etc.), you might wonder if .editorconfig is redundant. For teams exclusively using Visual Studio Code, the workspace settings will suffice. However, .editorconfig remains valuable if any team members use other editors like Vim, Sublime Text, or JetBrains IDEs. Using both ensures consistent formatting regardless of which editor each contributor prefers.
To help keep .editorconfig and .vscode/settings.json in sync, consider adding a comment in each file referencing the other. For example, add // Keep in sync with .editorconfig at the top of your settings.json, and # Keep in sync with .vscode/settings.json in your .editorconfig. This serves as a reminder when editing either file.
Checking and converting file encoding
To check the encoding of an existing file in PowerShell:
$bytes = [System.IO.File]::ReadAllBytes("C:\Scripts\MyScript.ps1")
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
Write-Host "UTF-8 with BOM"
} elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
Write-Host "UTF-16 LE (Unicode)"
} else {
Write-Host "UTF-8 without BOM (or ASCII)"
}Check file encoding in PowerShell
To convert a file to UTF-8 without BOM:
$content = Get-Content -Path "C:\Scripts\MyScript.ps1" -Raw
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText("C:\Scripts\MyScript.ps1", $content, $utf8NoBom)Convert file to UTF-8 without BOM in PowerShell
To normalize line endings to LF:
$content = Get-Content -Path "C:\Scripts\MyScript.ps1" -Raw
$content = $content -replace "`r`n", "`n"
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText("C:\Scripts\MyScript.ps1", $content, $utf8NoBom)Normalize line endings to LF in PowerShell
Summary of recommended file settings
For consistent, cross-platform PowerShell scripts, use these settings:
| Setting | Recommended Value | Reason |
|---|---|---|
| Encoding | UTF-8 without BOM | Cross-platform compatibility |
| Line endings | LF | Git-friendly, works on all platforms |
| Final newline | Yes | POSIX standard, cleaner diffs |
| Trailing whitespace | Remove | Cleaner code, smaller diffs |
| Indentation | 4 spaces | PowerShell community standard |
By combining PSScriptAnalyzer validation with proper file encoding and line endings, you ensure your PowerShell scripts are not only well-written but also portable and version control friendly.
Adding badges to your GitHub repository
If you share your PowerShell code on GitHub, you can add badges to your README to show that your code meets quality standards. Badges demonstrate your commitment to code quality and help others quickly understand the state of your project.
PSScriptAnalyzer badge
Shields.io is a service that generates badges for your projects. You can create a static PSScriptAnalyzer badge to add to your README:
This generates a badge that looks like this:
You can customize the badge to reflect different statuses:
| Status | Badge Code |
|---|---|
| Passed |  |
| Warnings |  |
| Failed |  |
CLM compatibility badge
If your scripts are designed to work in Constrained Language Mode, you can add a badge to indicate this:
{}This generates a badge that looks like this:
You can combine both badges to show code quality and CLM compatibility together.
Quick reference
Here is a summary of the key practices covered in this post:
| Area | Key Action | Why It Matters |
|---|---|---|
| Structure | Use regions, script headers, and snippets | Organizes code for readability and maintenance |
| Documentation | Add comment-based help to all functions | Makes code maintainable and discoverable |
| Validation | Use PSScriptAnalyzer in Visual Studio Code or CI/CD | Catches errors early, enforces best practices |
| CLM Compatibility | Test scripts in constrained environments | Ensures scripts work with WDAC/AppLocker |
| File Settings | Configure UTF-8 without BOM, LF line endings early | Cross-platform and version control friendly |
The wrap
Writing quality PowerShell code goes beyond just getting your scripts to work. It requires a combination of validation, documentation, compatibility awareness, and proper file handling.
PSScriptAnalyzer is the foundation of PowerShell code quality. It catches errors early, enforces best practices, and helps you write secure, maintainable scripts. Whether you use it from the command line, integrate it into Visual Studio Code, or automate it in your CI/CD pipelines, PSScriptAnalyzer should be part of every PowerShell developer’s toolkit.
Good documentation through comment-based help, meaningful inline comments, and organized code regions makes your scripts understandable and maintainable. Code that is well-documented today saves hours of confusion tomorrow, both for your colleagues and your future self.
Understanding Constrained Language Mode is equally important for enterprise environments. As organizations increasingly adopt security policies like WDAC and AppLocker, writing CLM-compatible scripts ensures your code works reliably across all environments, including those with the strictest security controls.
Finally, proper file encoding and line endings may seem like minor details, but they matter for cross-platform compatibility and team collaboration. UTF-8 without BOM and LF line endings are the modern standards that keep your scripts portable and version control friendly.
By combining these practices, validating with PSScriptAnalyzer, documenting thoroughly, testing for CLM compatibility, and using proper file settings, you produce PowerShell code that is not only functional but professional, secure, and ready for any environment.
Happy coding.
–Jesper

