Letting mere mortals run Windows PowerShell scripts
Tags: windows, rakulang
Published: 2024-04-26 12:57:27

---
title: Letting mere mortals run Windows PowerShell scripts
published: true
date: 2024-03-26 23:00:00 UTC
tags: Windows,Rakulang
canonical_url: https://peateasea.de/letting-mere-mortals-run-windows-powershell-scripts/
cover_image: https://peateasea.de/assets/images/windows-powershell-error.png
---

*Originally published on [peateasea.de](https://peateasea.de/letting-mere-mortals-run-windows-powershell-scripts/).*

While playing with [Raku](https://raku.org/) and [`rakubrew`](https://rakubrew.org/?platform=win) on Windows recently, I encountered more than one stumbling block. One stuck out in particular. Did you know that, by default, Windows PowerShell doesn’t allow normal users to run scripts? Coming from a Unix background, that surprised me. You might come across this issue when setting up `rakubrew` in Windows PowerShell, hence I thought I’d share my solution.

## An unexpected error

I’ll admit it: the issue I describe here doesn’t have all that much to do with Raku. It has much more to do with me stumbling over default Windows policies. It’s something I noticed after installing `rakubrew` in Windows PowerShell and hence could affect others. This might stop them from installing `rakubrew` and they may well avoid learning Raku. That [would be bad](https://peateasea.de/assets/images/ghostbusters-it-would-be-bad.gif).

The issue appeared after [installing `rakubrew` in PowerShell on Windows](https://rakubrew.org/?platform=win):

```powershell
# install rakubrew
. {iwr -useb https://rakubrew.org/install-on-powershell.ps1 } | iex
# add rakubrew initialisation code to PowerShell startup script
New-Item -Path (Split-Path $PROFILE) -ItemType "Directory" -Force
Add-Content -Force -Path $PROFILE -Value '. "C:\rakubrew\bin\rakubrew.exe" init PowerShell | Out-String | Invoke-Expression'
```

I started a fresh shell to initialise the `rakubrew` environment, yet I received this error message instead:

```
. : File C:\Users\cochrane\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded because
running scripts is disabled on this system. For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:3
+ . 'C:\Users\cochrane\Documents\WindowsPowerShell\Microsoft.PowerShell ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
```

This surprised me. I’d expected the shell to start and provide me with the `rakubrew` command so I could get on with my day. Why did the installation and setup instructions (seemingly) not work? The reason had to do with Windows PowerShell execution policies.

## A shortcut to a solution

The [default execution policy for Windows clients is `Restricted`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4#powershell-execution-policies), forbidding users from running scripts. The documentation states that this setting:

> - Permits individual commands, but does not allow scripts.
> - Prevents running of all script files, including formatting and configuration files (`.ps1xml`), module script files (`.psm1`), and PowerShell profiles (`.ps1`).

The solution is to allow the current user to run scripts. Giving users the `RemoteSigned` permission<sup id="fnref:remotesigned-odd-name" role="doc-noteref"><a href="#fn:remotesigned-odd-name" rel="footnote">1</a></sup> is sufficient for that purpose. To set this policy, run:

```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```

Now a ~~mere mortal~~ normal user can run script files. Also, the `rakubrew` initialisation now runs without error when Windows PowerShell starts. Yay! `\o/`

## Many details

Of course, the situation is much more nuanced than the terse solution described above. For those so inclined, I provide more background information than you thought you wanted. Details are often critical when dealing with computing systems, hence why I mention them here. And, as usual, when I have the opportunity to investigate something like this, I’m thorough!

### Being direct gets you what you want

Did you notice that we run a script as part of the `rakubrew` installation, i.e.

```powershell
. {iwr -useb https://rakubrew.org/install-on-powershell.ps1 } | iex
```

but it _doesn’t_ cause an error? Doesn’t that seem a bit weird to you? Well, technically the script _isn’t_ run by Windows PowerShell; its contents are passed to `iex`, which executes them. Since we’re running these commands by hand, we’re [allowed to run them](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4#powershell-execution-policies)<sup id="fnref:individual-commands-allowed" role="doc-noteref"><a href="#fn:individual-commands-allowed" rel="footnote">2</a></sup>. This explains why we _don’t_ see an error when installing `rakubrew`.

### Fear its presence

The remaining `rakubrew` installation steps create the Windows PowerShell user directory

```powershell
New-Item -Path (Split-Path $PROFILE) -ItemType "Directory" -Force
```

and then the Windows PowerShell profile file

```powershell
Add-Content -Force -Path $PROFILE -Value '. "C:\rakubrew\bin\rakubrew.exe" init PowerShell | Out-String | Invoke-Expression'
```

filling it with the content

```powershell
. "C:\rakubrew\bin\rakubrew.exe" init PowerShell | Out-String | Invoke-Expression
```

The content isn’t important for the execution policy error to occur: it’s the presence of the profile file itself.

You can try this at home. Enable the default execution policy like so:

```powershell
Set-ExecutionPolicy -ExecutionPolicy Default -Scope CurrentUser
```

This sets the `CurrentUser`’s policy to `Restricted`, which the `Get-ExecutionPolicy -List` command shows:

```powershell
PS C:\Users\cochrane> Get-ExecutionPolicy -List

        Scope ExecutionPolicy
        ----- ---------------
MachinePolicy Undefined
   UserPolicy Undefined
      Process Undefined
  CurrentUser Restricted
 LocalMachine Undefined
```

Windows PowerShell expects its profile file to be at the location in the `$PROFILE` environment variable. You can find out the value of this environment variable by typing its name into the shell:

```powershell
PS C:\Users\cochrane> $PROFILE
C:\Users\cochrane\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
```

Now create an empty file at this location e.g. via the `New-Item` command:

```powershell
PS C:\Users\cochrane> New-Item $PROFILE

    Directory: C:\Users\cochrane\Documents\WindowsPowerShell

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 26/03/2024 17:21 0 Microsoft.PowerShell_profile.ps1
```

Starting a fresh Windows PowerShell session greets us with our now familiar error message:

```powershell
. : File
C:\Users\cochrane\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded because
running scripts is disabled on this system. For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:3
+ . 'C:\Users\cochrane\Documents\WindowsPowerShell\Microsoft.PowerShell ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
```

Thus only the presence of this file causes the error when a user is `Restricted`.

### A shell by any other version

One suspicion I had for why this problem appeared was that Windows PowerShell was too old, or perhaps there was a problem with my system.

Checking my Windows PowerShell version metadata showed that it might be a bit old:

```powershell
PS C:\Users\cochrane> $PSVersionTable

Name Value
---- -----
PSVersion 5.1.19041.4170
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.4170
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
```

I’m running Windows 10 and the system is completely up-to-date, so things _should_ be ok.

Microsoft also mentions that there [are few differences in the PowerShell language between Windows PowerShell and PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell?view=powershell-7.4). Thus it’s unlikely that the Windows PowerShell version is causing this issue or is in any way special.

It’s also possible that this is just a case of [PEBKAC](https://peateasea.de/assets/images/pebkac.gif) and I changed some setting ages ago that I now can’t recall. Despite that, I rarely use PowerShell and consider this explanation unlikely.

From the information I was able to find, my version of Windows PowerShell seems to be fine.

### Different PowerShell, different behaviour

Of course, as soon as I started wondering if the problem was due to an outdated version, I considered upgrading. [Because they are two different projects](https://learn.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell?view=powershell-7.4), one can install PowerShell version 7 in parallel to Windows PowerShell, which I then did.

We have to be careful when using the name “PowerShell” because we need to differentiate between the two shells. Due to the similar names, I use “PowerShell” to refer to the new shell and “Windows PowerShell” to refer to the legacy program.<sup id="fnref:windows-powershell-with-win10" role="doc-noteref"><a href="#fn:windows-powershell-with-win10" rel="footnote">3</a></sup>

Get this though: PowerShell has a different default execution policy to Windows PowerShell 🙄.

Here is PowerShell’s default execution policy list:

```powershell
PS C:\Users\cochrane> Get-ExecutionPolicy -List

        Scope ExecutionPolicy
        ----- ---------------
MachinePolicy Undefined
   UserPolicy Undefined
      Process Undefined
  CurrentUser Undefined
 LocalMachine RemoteSigned
```

And here is Windows PowerShell’s default execution policy list:

```powershell
PS C:\Users\cochrane> Get-ExecutionPolicy -List

        Scope ExecutionPolicy
        ----- ---------------
MachinePolicy Undefined
   UserPolicy Undefined
      Process Undefined
  CurrentUser Restricted
 LocalMachine Undefined
```

Note how PowerShell has the policy `RemoteSigned` in the `LocalMachine` scope. This means anyone on the local machine can run scripts. I discovered this after a fresh install of PowerShell. The behaviour thus [directly contradicts the PowerShell documentation!](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4)

So, after installing `rakubrew` and adding its startup code to the profile file, PowerShell sessions start without a problem. It’s what any new `rakubrew` user would want.

### Grokking the docs (or trying to)

Computer systems are complex, intricate beasts and it can be hard to find relevant information in the documentation. For instance, although the [Windows Execution Policy documentation](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4) states that

> The execution policy for a particular session is stored only in memory and is lost when the session is closed.

the execution policy I set in Windows PowerShell was persistent; even across reboots. I don’t know what the docs are trying to tell me here. [Call me confused](https://peateasea.de/assets/images/surely-you-cant-be-serious.gif). :confused:

So what is the default Windows PowerShell execution policy? The [introductory (long description) section of the PowerShell documentation](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4#long-description) states that

> On non-Windows computers, the default execution policy is `Unrestricted` and cannot be changed.

But this section doesn’t mention what the default execution policy is for Windows computers. Where is that information hiding?

One has to [dig a bit harder](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4#powershell-execution-policies) to find it:

> `Default`
> 
> - Sets the default execution policy.
> - `Restricted` for Windows clients.
> - `RemoteSigned` for Windows servers.

Thus, one can run scripts on Windows servers (`RemoteSigned`) but not as a normal user on Windows client computers (`Restricted`).

Further digging uncovered more nuggets of information:

> `Restricted`
> 
> - The default execution policy for Windows client computers.

including:

> If no execution policy is set in any scope, the effective execution policy is `Restricted`, which is the default for Windows clients.

So, the default execution policy on Windows clients (presumably for the `CurrentUser` or the `LocalMachine`) is `Restricted`. I think it would be helpful to mention this in the introductory description. Even so, taking the time to peruse the documentation did help me form [a solution to my permissions problem](#a-shortcut-to-a-solution).

### Defaulting on a promise

Setting the `Default` policy without setting an explicit scope<sup id="fnref:localmachine-scope-denied" role="doc-noteref"><a href="#fn:localmachine-scope-denied" rel="footnote">4</a></sup> gave this error

```powershell
PS C:\Users\cochrane> Set-ExecutionPolicy -ExecutionPolicy Default

Execution Policy Change
The execution policy helps protect you from scripts that you do not trust. Changing the execution policy might expose
you to the security risks described in the about_Execution_Policies help topic at
https:/go.microsoft.com/fwlink/?LinkID=135170. Do you want to change the execution policy?
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "N"): Y
Set-ExecutionPolicy : Access to the registry key
'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell' is denied. To change the execution
policy for the default (LocalMachine) scope, start Windows PowerShell with the "Run as administrator" option. To
change the execution policy for the current user, run "Set-ExecutionPolicy -Scope CurrentUser".
At line:1 char:1
+ Set-ExecutionPolicy -ExecutionPolicy Default
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : PermissionDenied: (:) [Set-ExecutionPolicy], UnauthorizedAccessException
    + FullyQualifiedErrorId : System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.SetExecutionPolicyCommand
```

Even so, it’s possible to set the execution policy for the current user:

```powershell
PS C:\Users\cochrane> Set-ExecutionPolicy -ExecutionPolicy Default -Scope CurrentUser

Execution Policy Change
The execution policy helps protect you from scripts that you do not trust.
Changing the execution policy might expose you to the security risks described in the about_Execution_Policies help topic at
https:/go.microsoft.com/fwlink/?LinkID=135170. Do you want to change the execution policy?
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "N"): Y
```

Checking the list of execution policies gives the value we expect from the documentation:

```powershell
PS C:\Users\cochrane> Get-ExecutionPolicy -List

        Scope ExecutionPolicy
        ----- ---------------
MachinePolicy Undefined
   UserPolicy Undefined
      Process Undefined
  CurrentUser Restricted
 LocalMachine Undefined
```

Making this change disallowed script execution (as expected) and gave the error that led me down this particular garden path. This leads me to believe that this could have been the setting causing my initial woes. Note also that setting this value to `Undefined` leads to the same error. With that set, the default execution policy kicks in, disallowing the user from running any scripts.

## Finding use in a jumble of trifles

So, what did we learn?

- `rakubrew` installations on Windows PowerShell can raise unexpected errors,
- the execution policy for Windows PowerShell must allow users to run scripts for `rakubrew` to work as expected,
- Windows PowerShell and PowerShell are [two separate things](https://learn.microsoft.com/en-us/powershell/scripting/what-is-windows-powershell?view=powershell-7.4), seemingly with different default behaviours,
- documentation doesn’t always match reality, and
- the devil is always in the details.

I hope this information was useful. Happy hacking!

1. I find this execution policy name rather opaque. It doesn’t explain its purpose clearly: that of allowing scripts to run. After a bit of digging in the documentation, I guess it means “be able to run stuff like on a Windows server”. A Windows server is usually a remote machine and in that environment, one would need to run scripts. It’s a tenuous theory at best, though. [↩](#fnref:remotesigned-odd-name)

2. The default execution policy permits users to run individual commands. [↩](#fnref:individual-commands-allowed)

3. I.e. bundled as part of Windows 10 and only available up to version 5.x. [↩](#fnref:windows-powershell-with-win10)

4. Setting the `Default` policy for the `LocalMachine` scope failed as it must be run as the administrator. Now I have this vague feeling that all `ExecutionPolicy` scopes had the value `Undefined` when I started down this path. Oh well. [↩](#fnref:localmachine-scope-denied)