
Environment Variables in PowerShell: $env: Syntax, Persistent Variables, and Profiles
Complete guide to environment variables in PowerShell. Covers $env: syntax, SetEnvironmentVariable for persistence, user vs machine scope, PATH manipulation, profiles, and comparison with CMD syntax.
Environment Variables in PowerShell
You fire up PowerShell for the first time, type set MY_VAR=value like you've done a thousand times in CMD, and... nothing works. Or worse, you get some cryptic output about Set-Variable cmdlets. Welcome to PowerShell, where everything you knew about environment variables just got an overhaul.
PowerShell handles environment variables differently than traditional Windows command shells. The syntax is cleaner, the scope management is more explicit, and once you get past the initial learning curve, you'll find it's actually more powerful for automation and scripting. Here's everything you need to know about working with environment variables in PowerShell.
The $env: Syntax: Session-Scoped Variables
PowerShell uses the $env: prefix to access environment variables. This is fundamentally different from CMD's set command and feels more like a proper scripting language.
Reading an environment variable is straightforward:
# Read a variable
$env:PATH
$env:USERNAME
$env:COMPUTERNAME
# Use it in a command
Write-Host "Hello, $env:USERNAME"
Setting a variable is just as simple:
# Set a session variable
$env:API_KEY = "sk-1234567890"
$env:DATABASE_URL = "postgresql://localhost/mydb"
# Verify it worked
echo $env:API_KEY
Here's the catch: these changes only last for the current PowerShell session. Close the window, and your variables are gone. This is actually the same behavior as CMD's set command, but PowerShell makes it more obvious that you're working with temporary session state.
For quick testing or one-off scripts, session-scoped variables are perfect. For anything that needs to persist across reboots or sessions, you need a different approach.
Making PowerShell Environment Variables Persistent
To set persistent environment variables in PowerShell, you need the [Environment]::SetEnvironmentVariable() method. This is more verbose than the $env: syntax, but it gives you explicit control over where the variable lives.
# Set a persistent user-level variable
[Environment]::SetEnvironmentVariable('API_KEY', 'sk-1234567890', 'User')
# Set a system-wide machine-level variable (requires admin)
[Environment]::SetEnvironmentVariable('JAVA_HOME', 'C:\Program Files\Java\jdk-17', 'Machine')
# Set a process-level variable (same as $env:, only lasts for session)
[Environment]::SetEnvironmentVariable('TEMP_VAR', 'temporary', 'Process')
The third parameter is the scope, and it completely changes where Windows stores the variable:
- Process: Current session only. Equivalent to
$env:VARNAME = "value" - User: Stored in HKCU registry, persists across sessions for your user account
- Machine: Stored in HKLM registry, applies system-wide for all users (requires administrator privileges)
PowerShell's approach is more verbose but also more explicit — which is actually a good thing when you're debugging config issues at 2 AM. You know exactly what scope you're targeting without having to remember which command or flag does what.
User vs Machine Scope: When to Use Each
Choosing between User and Machine scope matters more than you might think. Get it wrong, and you'll either hit permission errors or create variables that don't apply where you need them.
Use User scope when:
- Setting up development environments for your personal projects
- Configuring API keys, tokens, or credentials that shouldn't be shared
- Installing tools that only you need (Node.js paths, Python virtual environments, etc.)
- You don't have admin rights on the machine
Use Machine scope when:
- Installing software that all users need (Java, Python, Node.js system installs)
- Configuring system-wide settings like proxy servers or network paths
- Setting up build servers, CI/CD agents, or shared workstations
- You need the variable available before any user logs in (for services or scheduled tasks)
Machine-level variables require administrator privileges. If you try to set one without admin rights, PowerShell will throw an access denied error. Run PowerShell as administrator (right-click > Run as Administrator) before attempting machine-level changes.
Here's a practical example: installing Node.js system-wide and adding it to PATH for all users.
# Run PowerShell as Administrator first
[Environment]::SetEnvironmentVariable(
'NODE_HOME',
'C:\Program Files\nodejs',
'Machine'
)
# Add to system PATH
$machinePath = [Environment]::GetEnvironmentVariable('PATH', 'Machine')
[Environment]::SetEnvironmentVariable(
'PATH',
"$machinePath;C:\Program Files\nodejs",
'Machine'
)
Reading Environment Variables Across Scopes
Here's where PowerShell's explicit scope system really shines. You can read variables from specific scopes without guessing where they're coming from.
# Read from User scope only
[Environment]::GetEnvironmentVariable('API_KEY', 'User')
# Read from Machine scope only
[Environment]::GetEnvironmentVariable('JAVA_HOME', 'Machine')
# Read from Process scope (current session, same as $env:)
[Environment]::GetEnvironmentVariable('TEMP_VAR', 'Process')
When you use $env:VARNAME, PowerShell resolves variables by merging all three scopes. Process scope takes precedence — if you set $env:PATH in your session, it overrides both User and Machine values for that session. The full resolution order (highest priority first):
- Process scope (current session) — wins over everything
- User scope (your account)
- Machine scope (system-wide)
This means session changes with $env: will temporarily override persistent settings, which is useful for testing without breaking your actual config.
Want to see where a variable is actually defined? Check all three scopes:
$varName = 'PATH'
Write-Host "Machine: $([Environment]::GetEnvironmentVariable($varName, 'Machine'))"
Write-Host "User: $([Environment]::GetEnvironmentVariable($varName, 'User'))"
Write-Host "Process: $([Environment]::GetEnvironmentVariable($varName, 'Process'))"
This is invaluable when debugging why a tool can't find an executable or why your application is picking up the wrong config.
Working with PATH in PowerShell
PATH manipulation is where most people run into trouble. PowerShell treats PATH like any other environment variable, but because it's a semicolon-delimited list, you need to be careful about how you modify it.
The wrong way to add to PATH:
# DON'T DO THIS - overwrites existing PATH
$env:PATH = "C:\MyApp\bin"
The right way is to append to the existing PATH:
# Session-scoped (temporary)
$env:PATH = "$env:PATH;C:\MyApp\bin"
# Persistent (User scope)
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
[Environment]::SetEnvironmentVariable(
'PATH',
"$userPath;C:\MyApp\bin",
'User'
)
Want to inspect what's in your PATH? Split it by semicolons to see each entry:
# Split PATH into individual entries
$env:PATH -split ';'
# Or more readable with foreach
$env:PATH -split ';' | ForEach-Object { Write-Host $_ }
# Find specific entries
$env:PATH -split ';' | Where-Object { $_ -like '*Python*' }
Common mistake: adding duplicate entries to PATH. Before appending, check if the path already exists:
$newPath = "C:\MyApp\bin"
$currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if ($currentPath -notlike "*$newPath*") {
[Environment]::SetEnvironmentVariable(
'PATH',
"$currentPath;$newPath",
'User'
)
Write-Host "Added $newPath to PATH"
} else {
Write-Host "$newPath already in PATH"
}
Removing a PATH entry is trickier but manageable with PowerShell's filtering:
$pathToRemove = "C:\MyApp\bin"
$currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
$newPath = ($currentPath -split ';' | Where-Object { $_ -ne $pathToRemove }) -join ';'
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
Setting Multiple Environment Variables
When you're configuring a development environment or deploying an application, you often need to set dozens of variables at once. PowerShell's hashtable syntax makes this clean:
# Define all your variables in a hashtable
$envVars = @{
'DATABASE_URL' = 'postgresql://localhost/mydb'
'REDIS_URL' = 'redis://localhost:6379'
'API_KEY' = 'sk-1234567890'
'LOG_LEVEL' = 'debug'
'PORT' = '3000'
}
# Set them all at User scope
foreach ($key in $envVars.Keys) {
[Environment]::SetEnvironmentVariable($key, $envVars[$key], 'User')
Write-Host "Set $key = $($envVars[$key])"
}
For session-only variables (testing configurations), it's even simpler:
$envVars = @{
'NODE_ENV' = 'development'
'DEBUG' = '*'
'API_ENDPOINT' = 'https://staging.api.example.com'
}
$envVars.GetEnumerator() | ForEach-Object {
Set-Item -Path "env:$($_.Key)" -Value $_.Value
}
This pattern is particularly useful in setup scripts or when switching between different project configurations. Store your environments in separate hashtables and load whichever one you need.
PowerShell Profiles: Auto-Loading Variables
If you find yourself setting the same environment variables every time you open PowerShell, you need a profile. PowerShell profiles are scripts that run automatically when you start a session — perfect for loading project-specific or machine-specific configuration.
First, check where your profile lives:
# Show profile path
$PROFILE
# Check if it exists
Test-Path $PROFILE
# Create it if it doesn't exist
if (!(Test-Path $PROFILE)) {
New-Item -Path $PROFILE -Type File -Force
}
# Edit it
notepad $PROFILE
Now add your variables to the profile:
# Microsoft.PowerShell_profile.ps1
# Development environment variables
$env:EDITOR = "code"
$env:NODE_ENV = "development"
$env:DOTNET_CLI_TELEMETRY_OPTOUT = "1"
# Project paths
$env:PROJECT_ROOT = "C:\Dev\MyProject"
$env:DATA_DIR = "C:\Data"
# API keys (if you're comfortable storing them here)
$env:GITHUB_TOKEN = "ghp_..."
Write-Host "PowerShell profile loaded" -ForegroundColor Green
PowerShell actually has four different profile scopes:
- AllUsersAllHosts: Applies to all users, all PowerShell hosts (Console, ISE, VSCode)
- AllUsersCurrentHost: Applies to all users, current host only
- CurrentUserAllHosts: Your account, all hosts
- CurrentUserCurrentHost: Your account, current host only (this is what
$PROFILEpoints to)
For most developers, $PROFILE (CurrentUserCurrentHost) is all you need. Save it, restart PowerShell, and your variables load automatically.
One warning: if your profile script has errors, PowerShell will still start but might fail silently. Test it with:
# Test your profile for syntax errors
$null = . $PROFILE
PowerShell vs CMD: Environment Variable Comparison
If you're coming from CMD or batch scripting, here's how the two approaches map to each other:
| Task | CMD | PowerShell |
|---|---|---|
| Read variable | echo %PATH% | $env:PATH or echo $env:PATH |
| Set session variable | set VAR=value | $env:VAR = "value" |
| Set persistent user variable | setx VAR value | [Environment]::SetEnvironmentVariable('VAR', 'value', 'User') |
| Set persistent machine variable | setx VAR value /M | [Environment]::SetEnvironmentVariable('VAR', 'value', 'Machine') |
| Append to PATH (session) | set PATH=%PATH%;C:\new | $env:PATH = "$env:PATH;C:\new" |
| Delete variable (session) | set VAR= | Remove-Item env:VAR |
| Delete persistent variable | setx VAR "" (doesn't actually delete) | [Environment]::SetEnvironmentVariable('VAR', $null, 'User') |
| List all variables | set | Get-ChildItem env: or dir env: |
The biggest difference: CMD's setx is fire-and-forget. It sets the variable in the registry but doesn't update the current session. PowerShell's [Environment]::SetEnvironmentVariable() does the same — you need to manually update $env:VAR if you want the change to take effect immediately:
# Set persistent variable
[Environment]::SetEnvironmentVariable('API_KEY', 'sk-1234567890', 'User')
# Also update current session so you can use it immediately
$env:API_KEY = 'sk-1234567890'
Or wrap it in a function for convenience:
function Set-EnvVar($name, $value, $scope = 'User') {
[Environment]::SetEnvironmentVariable($name, $value, $scope)
Set-Item -Path "env:$name" -Value $value
Write-Host "Set $name in $scope scope and current session"
}
# Usage
Set-EnvVar -name 'DATABASE_URL' -value 'postgresql://localhost/mydb'
Managing Variables Across Teams and Environments
When your team works across PowerShell, Bash, and different environments, keeping variables in sync gets messy fast. Developer A sets API_ENDPOINT in PowerShell. Developer B uses Bash and names it API_URL. Production uses something completely different. Then someone rotates a key and you spend the afternoon tracking down which script didn't get updated.
EnvManager (free tier available) lets you manage environment variables from one place and pull them into any shell. Define your variables once, sync them across PowerShell, Bash, CI/CD pipelines, and production servers. No more copy-pasting .env files or maintaining separate shell scripts for each environment.
You're still using PowerShell locally — you're just pulling the source of truth from a central config instead of hardcoding it in your profile or scripts. Rotate a secret in production, and it's immediately available to everyone who needs it, regardless of which shell or platform they're using.
For smaller projects or solo work, PowerShell profiles work fine. For teams with multiple environments and compliance requirements, centralized management saves more time than you'd expect.
PowerShell's environment variable system is more explicit than CMD but also more powerful once you understand the scope model. Use $env: for quick session work, [Environment]::SetEnvironmentVariable() when you need persistence, and profiles when you want automation. And if you're managing variables across multiple people, platforms, or deployment targets, invest in proper tooling before it becomes a problem.
Related reading:
- How to Set Environment Variables — Cross-platform guide covering Windows, macOS, and Linux
- How to Set Environment Variables in Windows — GUI methods, registry editing, and system settings
- How to Set Environment Variables in Linux — Bash profiles, systemd, and shell-specific configuration
- Environment Variables in Python — os.environ, python-dotenv, and best practices
- Git Environment Variables — Identity, SSH keys, debugging, and CI/CD usage