Skip to content

Instantly share code, notes, and snippets.

@rajbos
Created July 15, 2020 14:17
Show Gist options
  • Select an option

  • Save rajbos/49f70f4e2b9765da05f0526225de2450 to your computer and use it in GitHub Desktop.

Select an option

Save rajbos/49f70f4e2b9765da05f0526225de2450 to your computer and use it in GitHub Desktop.
Register Windows Startup/Shutdown script
function Register-EventScript {
param (
[string] $eventToRegister, # Either Startup or Shutdown
[string] $pathToScript,
[string] $scriptParameters
)
$path = "$ENV:systemRoot\System32\GroupPolicy\Machine\Scripts\$eventToRegister"
if (-not (Test-Path $path)) {
# path HAS to be available for this to work
New-Item -path $path -itemType Directory
}
# Add script to Group Policy through the Registry
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\$eventToRegister\0\0",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\$eventToRegister\0\0" |
ForEach-Object {
if (-not (Test-Path $_)) {
New-Item -path $_ -force
}
}
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\$eventToRegister\0",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\$eventToRegister\0" |
ForEach-Object {
New-ItemProperty -path "$_" -name DisplayName -propertyType String -value "Local Group Policy" -force
New-ItemProperty -path "$_" -name FileSysPath -propertyType String -value "$ENV:systemRoot\System32\GroupPolicy\Machine" -force
New-ItemProperty -path "$_" -name GPO-ID -propertyType String -value "LocalGPO" -force
New-ItemProperty -path "$_" -name GPOName -propertyType String -value "Local Group Policy" -force
New-ItemProperty -path "$_" -name PSScriptOrder -propertyType DWord -value 2 -force
New-ItemProperty -path "$_" -name SOM-ID -propertyType String -value "Local" -force
}
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\$eventToRegister\0\0",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\$eventToRegister\0\0" |
ForEach-Object {
New-ItemProperty -path "$_" -name Script -propertyType String -value $pathToScript -force
New-ItemProperty -path "$_" -name Parameters -propertyType String -value $scriptParameters -force
New-ItemProperty -path "$_" -name IsPowershell -propertyType DWord -value 1 -force
New-ItemProperty -path "$_" -name ExecTime -propertyType QWord -value 0 -force
}
}
# get crrentlocation so we can form the full path to the script to register
$currentLocation = $PSScriptRoot
# register the script twice
Register-EventScript -eventToRegister "Startup" -pathToScript "$currentLocation\ScriptToRun.ps1" -scriptParameters "OnStartup"
Register-EventScript -eventToRegister "Shutdown" -pathToScript "$currentLocation\ScriptToRun.ps1" -scriptParameters "OnShutdown"
param (
[string] $message
)
function Run-Script {
param (
[string] $message
)
# get the date in our format
$date = Get-Date
Get-date $date -f "yyyyMMdd HH:mm:ss"
# log the date/time and the message
"$($date.ToUniversalTime().ToString("yyyyMMdd HH:mm:ss")) - $message" >> "C:\Temp\Log.txt"
}
# call the script with the incoming parameter
Run-Script -message $message
@rajbos
Copy link
Author

rajbos commented Jul 15, 2020

Working

The Register script will register the ScriptToRun.ps1 for both Machine Startup and Shutdown.
The ScriptToRun will then log those events to a text file for easy testing.

Tested

Tested on a Windows 10 VM (1909)

Caveat:

You need to run the register script with an elevated session

@BasedUser
Copy link

BasedUser commented Sep 18, 2024

Also works on Windows 10, native, 22H2 (10.0.19045.4780).

@jeremiassamuelzitnik
Copy link

Also Works on Windows 11 24H2.

Is posible to check if previously exists other ID and increment without removing previous one?

@jeremiassamuelzitnik
Copy link

jeremiassamuelzitnik commented Sep 11, 2025

Here is my bit. It do it incremental

function Register-EventScript {
    param (
        [string] $eventToRegister, # Either Startup or Shutdown
        [string] $pathToScript,
        [string] $scriptParameters
    )
    $path = "$ENV:systemRoot\System32\GroupPolicy\Machine\Scripts\$eventToRegister"
    if (-not (Test-Path $path)) {
        # Path must exist for this to work
        New-Item -Path $path -ItemType Directory -Force
    }

    # Define base registry paths
    $baseRegPaths = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\$eventToRegister",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\$eventToRegister"
    )

    # Find the next available index for the script
    $index = 0
    foreach ($basePath in $baseRegPaths) {
        $existingKeys = Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match '^\d+$' } | Select-Object -ExpandProperty PSChildName
        if ($existingKeys) {
            $maxIndex = ($existingKeys | ForEach-Object { [int]$_ } | Measure-Object -Maximum).Maximum
            $index = [math]::Max($index, $maxIndex + 1)
        }
    }

    # Create registry paths with the new index
    $regPaths = $baseRegPaths | ForEach-Object { "$_\$index" }
    $scriptRegPaths = $baseRegPaths | ForEach-Object { "$_\$index\0" }

    # Create registry keys for the new index
    foreach ($regPath in $regPaths) {
        if (-not (Test-Path $regPath)) {
            New-Item -Path $regPath -Force
        }
        # Set properties for the parent key
        New-ItemProperty -Path $regPath -Name DisplayName -PropertyType String -Value "Local Group Policy" -Force
        New-ItemProperty -Path $regPath -Name FileSysPath -PropertyType String -Value "$ENV:systemRoot\System32\GroupPolicy\Machine" -Force
        New-ItemProperty -Path $regPath -Name GPO-ID -PropertyType String -Value "LocalGPO" -Force
        New-ItemProperty -Path $regPath -Name GPOName -PropertyType String -Value "Local Group Policy" -Force
        New-ItemProperty -Path $regPath -Name PSScriptOrder -PropertyType DWord -Value 2 -Force
        New-ItemProperty -Path $regPath -Name SOM-ID -PropertyType String -Value "Local" -Force
    }

    # Create subkeys and set properties for the script
    foreach ($scriptRegPath in $scriptRegPaths) {
        if (-not (Test-Path $scriptRegPath)) {
            New-Item -Path $scriptRegPath -Force
        }
        New-ItemProperty -Path $scriptRegPath -Name Script -PropertyType String -Value $pathToScript -Force
        New-ItemProperty -Path $scriptRegPath -Name Parameters -PropertyType String -Value $scriptParameters -Force
        New-ItemProperty -Path $scriptRegPath -Name IsPowershell -PropertyType DWord -Value 1 -Force
        New-ItemProperty -Path $scriptRegPath -Name ExecTime -PropertyType QWord -Value 0 -Force
    }
}

# get crrentlocation so we can form the full path to the script to register
$currentLocation = $PSScriptRoot

Register-EventScript -eventToRegister "Startup" -pathToScript "$currentLocation\ScriptToRun.ps1" -scriptParameters "Startup"
Register-EventScript -eventToRegister "Shutdown" -pathToScript "$currentLocation\ScriptToRun.ps1" -scriptParameters "Shutdown"

@jeremiassamuelzitnik
Copy link

And this is for unregister:

function Unregister-EventScript {
    param (
        [string] $eventToUnregister # Either Startup or Shutdown
    )

    # Base registry paths where scripts are registered
    $baseRegPaths = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\${eventToUnregister}",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\${eventToUnregister}"
    )

    # Collect all registered scripts
    $scripts = @()
    foreach ($basePath in $baseRegPaths) {
        if (Test-Path $basePath) {
            Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue | ForEach-Object {
                $index = $_.PSChildName
                $scriptPath = (Get-ItemProperty -Path "$basePath\$index\0" -ErrorAction SilentlyContinue).Script
                $params     = (Get-ItemProperty -Path "$basePath\$index\0" -ErrorAction SilentlyContinue).Parameters

                if ($scriptPath) {
                    $scripts += [PSCustomObject]@{
                        Event      = $eventToUnregister
                        Index      = $index
                        ScriptPath = $scriptPath
                        Parameters = $params
                        RegBase    = $basePath
                    }
                }
            }
        }
    }

    if (-not $scripts) {
        Write-Host "No scripts found for ${eventToUnregister}"
        return
    }

    # Show scripts and ask the user which ones to remove
    Write-Host "`nScripts found for event ${eventToUnregister}:`n"
    $i = 0
    $scripts | ForEach-Object {
        Write-Host "[$i] $($_.ScriptPath) $($_.Parameters)"
        $i++
    }

    $selection = Read-Host "Enter the number of the script to remove (comma-separated for multiple, or 'all' for all)"
    if ($selection -eq "all") {
        $toRemove = $scripts
    }
    else {
        $indexes = $selection -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^\d+$' }
        $toRemove = $scripts[$indexes]
    }

    # Remove selected scripts
    foreach ($s in $toRemove) {
        $regPath = "$($s.RegBase)\$($s.Index)"
        if (Test-Path $regPath) {
            Remove-Item -Path $regPath -Recurse -Force
            Write-Host "Removed: $($s.ScriptPath) [${eventToUnregister}]"
        }
    }
}

# Example usage:
Unregister-EventScript -eventToUnregister "Startup"
Unregister-EventScript -eventToUnregister "Shutdown"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment