Configure Edge and Brave too, not just Chrome

They are Chromium: same CRX, same update manifest, same policy — only the
registry vendor key differs. The script now defaults to every one of the three
it finds installed, which is what the Linux counterpart already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-31 13:08:29 -05:00
co-authored by Claude Opus 5
parent b9da419ebc
commit b820168afa
3 changed files with 101 additions and 54 deletions
+6 -2
View File
@@ -93,12 +93,16 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-po
sudo scripts/install-extension-policy.sh sudo scripts/install-extension-policy.sh
``` ```
Both configure every Chromium-family browser they find — Chrome, Edge and Brave
take the same CRX, the same update manifest and the same policy, differing only
in where that policy is written. Narrow it with `-Browser Edge` if you would
rather not.
The Windows script writes to the current user's policy hive and needs no The Windows script writes to the current user's policy hive and needs no
elevation; `-Scope Machine` covers every account on the box and does need an elevation; `-Scope Machine` covers every account on the box and does need an
elevated shell. Linux has no per-user equivalent — Chrome only reads managed elevated shell. Linux has no per-user equivalent — Chrome only reads managed
policy from `/etc` — so that one is root-only and machine-wide. Both take policy from `/etc` — so that one is root-only and machine-wide. Both take
`-Remove` / `--remove`, and the Windows one takes `-Browser Edge` or `-Remove` / `--remove`.
`-Browser Brave`.
Afterwards, remove the unpacked copy from `chrome://extensions` — it now has the Afterwards, remove the unpacked copy from `chrome://extensions` — it now has the
same id, and Chrome will not load both — and restart the browser. same id, and Chrome will not load both — and restart the browser.
+77 -35
View File
@@ -3,12 +3,16 @@
Install the AutoFirmer extension from our own update server, via policy. Install the AutoFirmer extension from our own update server, via policy.
.DESCRIPTION .DESCRIPTION
Chrome will not side-load an extension on Windows — dropping a CRX on Chrome will not side-load a CRX on Windows — dropping one on
chrome://extensions is refused — and it will not update an unpacked one. The chrome://extensions is refused — and it will not update an unpacked folder.
supported way to run an extension you host yourself is a policy: The supported way to run an extension you host yourself is a policy:
ExtensionInstallForcelist names the extension id and the update manifest, and ExtensionInstallForcelist names the extension id and the update manifest, and
from then on Chrome installs it, keeps it updated on its own schedule, and from then on the browser installs it, keeps it updated on its own schedule,
re-installs it if it goes missing. and re-installs it if it goes missing.
Edge and Brave are Chromium too: same CRX, same policy, same update
manifest, only a different registry vendor key. By default this configures
every one of the three that is actually installed.
Defaults to the current user's policy hive, which needs no elevation. Defaults to the current user's policy hive, which needs no elevation.
-Scope Machine writes to HKLM instead, for every account on the box; that -Scope Machine writes to HKLM instead, for every account on the box; that
@@ -16,8 +20,8 @@
After this runs: After this runs:
* Remove the unpacked copy from chrome://extensions. It has the same id * Remove the unpacked copy from chrome://extensions. It has the same id
now, and Chrome will not load both. now, and the browser will not load both.
* Restart Chrome, or open chrome://policy and press "Reload policies". * Restart the browser, or open chrome://policy and press "Reload policies".
* A force-installed extension cannot be disabled or removed from the * A force-installed extension cannot be disabled or removed from the
extensions page. Re-run with -Remove to take it off. extensions page. Re-run with -Remove to take it off.
@@ -25,12 +29,12 @@
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1
.EXAMPLE .EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 -Scope Machine -Browser Edge powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 -Browser Edge -Scope Machine
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[ValidateSet('Chrome', 'Edge', 'Brave')] [ValidateSet('All', 'Chrome', 'Edge', 'Brave')]
[string]$Browser = 'Chrome', [string]$Browser = 'All',
[ValidateSet('User', 'Machine')] [ValidateSet('User', 'Machine')]
[string]$Scope = 'User', [string]$Scope = 'User',
@@ -45,11 +49,28 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# Vendor key under SOFTWARE\Policies, and the executable each one registers.
$vendors = @{ $vendors = @{
Chrome = 'Google\Chrome' Chrome = 'Google\Chrome'
Edge = 'Microsoft\Edge' Edge = 'Microsoft\Edge'
Brave = 'BraveSoftware\Brave' Brave = 'BraveSoftware\Brave'
} }
$exes = @{
Chrome = 'chrome.exe'
Edge = 'msedge.exe'
Brave = 'brave.exe'
}
function Test-BrowserInstalled([string]$Name) {
# App Paths is where every one of these registers itself, per-machine or
# per-user depending on how it was installed.
foreach ($hive in @('HKLM:', 'HKCU:')) {
if (Test-Path "$hive\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$($exes[$Name])") {
return $true
}
}
return $false
}
$hive = if ($Scope -eq 'Machine') { 'HKLM:' } else { 'HKCU:' } $hive = if ($Scope -eq 'Machine') { 'HKLM:' } else { 'HKCU:' }
@@ -61,55 +82,76 @@ if ($Scope -eq 'Machine') {
} }
} }
$key = "$hive\SOFTWARE\Policies\$($vendors[$Browser])\ExtensionInstallForcelist" $targets = if ($Browser -eq 'All') {
@('Chrome', 'Edge', 'Brave') | Where-Object { Test-BrowserInstalled $_ }
} else {
@($Browser)
}
if (-not $targets) {
throw 'Found no Chrome, Edge or Brave installation. Name one with -Browser if it is installed somewhere unusual.'
}
$entry = "$Id;$UpdateUrl" $entry = "$Id;$UpdateUrl"
if ($Remove) { foreach ($target in $targets) {
if (-not (Test-Path $key)) { Write-Host "Nothing to remove — no $Scope forcelist for $Browser."; exit 0 } $key = "$hive\SOFTWARE\Policies\$($vendors[$target])\ExtensionInstallForcelist"
if ($Remove) {
if (-not (Test-Path $key)) {
Write-Host "$target : nothing to remove — no $Scope forcelist."
continue
}
$props = Get-ItemProperty $key $props = Get-ItemProperty $key
$found = $false $found = $false
foreach ($name in ($props.PSObject.Properties.Name | Where-Object { $_ -notlike 'PS*' })) { foreach ($name in ($props.PSObject.Properties.Name | Where-Object { $_ -notlike 'PS*' })) {
if ($props.$name -like "$Id;*") { if ($props.$name -like "$Id;*") {
Remove-ItemProperty -Path $key -Name $name Remove-ItemProperty -Path $key -Name $name
Write-Host "Removed $name ($($props.$name))" Write-Host "$target : removed entry $name ($($props.$name))"
$found = $true $found = $true
} }
} }
if (-not $found) { Write-Host "Nothing to remove — $Id was not in the $Browser $Scope forcelist." } if (-not $found) { Write-Host "$target : nothing to remove — $Id was not in the $Scope forcelist." }
Write-Host "`nRestart $Browser to let the change take effect." continue
exit 0 }
}
if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null } if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null }
# The forcelist is a list expressed as numbered values, so adding to it means # The forcelist is a list expressed as numbered values, so adding to it means
# finding a free number rather than overwriting whatever is already there — # finding a free number rather than overwriting whatever is already there —
# another extension may well be in the list. # another extension may well be in the list.
$props = Get-ItemProperty $key $props = Get-ItemProperty $key
$existing = $props.PSObject.Properties | $existing = $props.PSObject.Properties |
Where-Object { $_.Name -notlike 'PS*' -and $_.Value -like "$Id;*" } Where-Object { $_.Name -notlike 'PS*' -and $_.Value -like "$Id;*" }
if ($existing) { if ($existing) {
Set-ItemProperty -Path $key -Name $existing.Name -Value $entry Set-ItemProperty -Path $key -Name $existing.Name -Value $entry
Write-Host "Updated the $Browser $Scope forcelist entry $($existing.Name):" Write-Host "$target : updated $Scope forcelist entry $($existing.Name)"
} else { } else {
$used = $props.PSObject.Properties.Name | $used = $props.PSObject.Properties.Name |
Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ } Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ }
$slot = if ($used) { (($used | Measure-Object -Maximum).Maximum + 1) } else { 1 } $slot = if ($used) { (($used | Measure-Object -Maximum).Maximum + 1) } else { 1 }
New-ItemProperty -Path $key -Name "$slot" -Value $entry -PropertyType String -Force | Out-Null New-ItemProperty -Path $key -Name "$slot" -Value $entry -PropertyType String -Force | Out-Null
Write-Host "Added $Browser $Scope forcelist entry ${slot}:" Write-Host "$target : added $Scope forcelist entry $slot"
}
Write-Host " $key"
}
if ($Remove) {
Write-Host "`nRestart the browser to let the change take effect."
exit 0
} }
Write-Host " $key"
Write-Host " $entry"
Write-Host @" Write-Host @"
$entry
Next: Next:
1. Remove the unpacked copy from chrome://extensions (same id — both cannot load). 1. Remove the unpacked copy from chrome://extensions (same id — both cannot load).
2. Restart $Browser, or open chrome://policy and press "Reload policies". 2. Restart the browser, or open its chrome://policy page and press "Reload policies".
3. Check chrome://extensions: the card should say "Installed by enterprise policy". 3. The card should read "Installed by enterprise policy".
From here $Browser updates it on its own. The extension also asks for a check as From here the browser updates it on its own. The extension also asks for a check
soon as the dashboard reports a newer version, so a release normally lands in as soon as the dashboard reports a newer version, so a release normally lands in
seconds rather than waiting out Chrome's own cycle. seconds rather than waiting out the browser's own cycle.
"@ "@
+4 -3
View File
@@ -331,9 +331,10 @@ REM the extension is installed the way a self-hosted one has to be: a policy
REM entry naming our update server. Written to this user's hive, which needs no REM entry naming our update server. Written to this user's hive, which needs no
REM elevation - install-extension-policy.ps1 -Scope Machine covers every account. REM elevation - install-extension-policy.ps1 -Scope Machine covers every account.
echo. echo.
echo [7/7] Chrome extension echo [7/7] Browser extension
echo Installs it from the update server and lets Chrome keep it current echo Installs it from the update server into whichever of Chrome, Edge
echo by itself. No administrator rights needed. echo and Brave are present, and lets them keep it current by themselves.
echo No administrator rights needed.
set "DOEXT=" set "DOEXT="
set /p "DOEXT= Set this up now? [Y/n] " set /p "DOEXT= Set this up now? [Y/n] "
if /i "%DOEXT%"=="n" goto :ext_skipped if /i "%DOEXT%"=="n" goto :ext_skipped