
Motivation
If you’ve ever needed to restrict which commands can be run inside a VS Code integrated terminal – nowadays mainly to prevent agents from wreaking havoc – you can achieve this using a combination of VS Code terminal profiles and PowerShell’s PSReadLine module. I’m not sure is/how this works with other terminals, however I’ve verified that it works both in Windows and Ubuntu with the snap package of PowerShell. The base idea is that any commands entered in the terminal go first through a script where some custom logic & regex do their magic.
Bootstrapping the guard (settings.json)
First, we need to ensure that every new terminal automatically loads our guard script. We can do this by defining a custom terminal profile in settings.json and making it the default.
{ "terminal.integrated.profiles.linux": { "Guarded PowerShell": { "path": "pwsh", "args": [ "-NoExit", "-Command", ". ./.vscode/terminal-guard.ps1" ] } }, "terminal.integrated.defaultProfile.linux": "Guarded PowerShell"}
This configuration launches pwsh, dot-sources our terminal-guard.ps1 script to keep its functions and variables in the global scope, and then keeps the session open (-NoExit).
Intercepting the Enter key (terminal-guard.ps1)
The core trick relies on PSReadLine, which handles console input in PowerShell. By overriding the Enter key handler, we can capture the buffer line, validate it against an allowlist, and either accept or reject it.
# Define approved regex patterns$script:ApprovedPatterns = @( "^\s*\.\\scripts\\[a-zA-Z0-9_-]+\.ps1\b" # Allow local scripts "^\s*Get-ChildItem\b" # Allow specific cmdlets "^\s*ls\b" # Allow basic utilities)# Intercept the Enter keySet-PSReadLineKeyHandler -Key Enter -ScriptBlock { $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) # Custom logic to split the line by semicolons and match against $ApprovedPatterns if (Test-CommandApproved $line) { [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() } else { [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() Write-Host "`nBLOCKED: Command is not in the approved whitelist." -ForegroundColor Red }}
Behind the scenes, the Test-CommandApproved function splits the input $line by semicolons to handle chained commands. It then evaluates the first word of each pipeline statement against the $ApprovedPatterns regex array.
If the command doesn’t match, RevertLine() is called. The command is cleared, a warning is printed to the console, and execution is completely halted.
The escape hatch
To ensure you don’t permanently lock yourself out, it’s wise to include an escape hatch. A simple Disable-TerminalGuard function that requires an interactive Read-Host prompt (e.g., typing “UNLOCK”) prevents automated agents from bypassing the guard, while allowing human developers to lift the restrictions when necessary.