Swapping Chrome for Brave on Windows

Swapping Chrome for Brave on Windows

Unfortunately, Google phased out Manifest V2 in Chrome, so the full version of uBlock Origin is no longer available:

In late 2024, Google completed its transition to Manifest V3 in Chrome, which fundamentally changed how browser extensions work. As a result, the full version of uBlock Origin is no longer available for Chrome users. Here's what happened and what your options are now.

Let's switch to Brave, a more privacy-focused browser.

Installation

We can install it from the command-line:

winget install Brave.Brave

Disable Brave features

I don't need Leo, Wallet, or the built-in VPN. We can disable these features with a registry policy for the current Windows user:

& {
    $ErrorActionPreference = 'Stop'
    if (!(whoami /groups | findstr S-1-16-12288)) {
        throw 'Run PowerShell as Administrator.'
    }
    $policyPath = 'HKCU:\SOFTWARE\Policies\BraveSoftware\Brave'

    New-Item -Path $policyPath -Force | Out-Null

    $policies = @{
        BraveAIChatEnabled  = 0
        BraveWalletDisabled = 1
        BraveVPNDisabled    = 1
    }

    foreach ($policy in $policies.GetEnumerator()) {
        New-ItemProperty `
            -Path $policyPath `
            -Name $policy.Key `
            -PropertyType DWord `
            -Value $policy.Value `
            -Force | Out-Null
    }

    Write-Host 'Brave policies applied successfully.'
    Write-Host 'Restart Brave for the changes to take effect.'
}

Restart Brave and check brave://policy to verify that the policies are active.

Create the profiles

I want two profiles: Personal and Company. This script lets Brave initialize any missing profiles, then configures their names and avatar slots. It leaves the avatar image files alone; we'll replace those manually in the next section.

& {
    $ErrorActionPreference = 'Stop'

    function Wait-BraveClosed {
        while (Get-Process brave -ErrorAction SilentlyContinue) {
            Read-Host 'Close Brave, then press Enter to continue'
        }
    }

    Wait-BraveClosed
    $brave = @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $env:LOCALAPPDATA) |
        ForEach-Object { "$_\BraveSoftware\Brave-Browser\Application\brave.exe" } |
        Where-Object { Test-Path $_ } | Select-Object -First 1
    if (-not $brave) { throw 'Brave could not be found.' }

    $userData = "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data"
    $profiles = @(
        @{ Name = 'Personal'; Directory = 'Default'; Index = 57 }
        @{ Name = 'Company'; Directory = 'Profile 1'; Index = 56 }
    )
    foreach ($profile in $profiles) {
        $name = Read-Host "Profile name [$($profile.Name)]"
        if (-not [string]::IsNullOrWhiteSpace($name)) { $profile.Name = $name }
    }

    function Wait-ForFile($Path, $Seconds = 15) {
        $timeout = [DateTime]::UtcNow.AddSeconds($Seconds)
        while (-not (Test-Path $Path) -and [DateTime]::UtcNow -lt $timeout) {
            Start-Sleep -Milliseconds 500
        }
        if (-not (Test-Path $Path)) { throw "File creation timed out: $Path" }
    }

    function Set-JsonProperty($Object, $Name, $Value) {
        if ($Object.PSObject.Properties.Name -contains $Name) {
            $Object.$Name = $Value
        }
        else {
            $Object | Add-Member NoteProperty $Name $Value
        }
    }

    function New-BraveProfile($Profile) {
        $preferences = "$userData\$($Profile.Directory)\Preferences"
        if (Test-Path $preferences) { return }

        Write-Host "Creating profile: $($Profile.Name)"
        $arguments = "--headless --dump-dom --user-data-dir=`"$userData`" --profile-directory=`"$($Profile.Directory)`" --no-first-run --no-default-browser-check about:blank"
        Start-Process $brave -ArgumentList $arguments -Wait | Out-Null
        Wait-ForFile $preferences
    }

    foreach ($profile in $profiles) { New-BraveProfile $profile }

    $statePath = "$userData\Local State"
    $state = Get-Content $statePath -Raw | ConvertFrom-Json
    if (-not $state.profile) { Set-JsonProperty $state 'profile' ([pscustomobject]@{}) }
    if (-not $state.profile.info_cache) {
        Set-JsonProperty $state.profile 'info_cache' ([pscustomobject]@{})
    }
    foreach ($profile in $profiles) {
        $preferences = "$userData\$($profile.Directory)\Preferences"
        Write-Host "Updating profile: $($profile.Name)"
        $prefs = Get-Content $preferences -Raw | ConvertFrom-Json
        Set-JsonProperty $prefs.profile 'name' $profile.Name
        Set-JsonProperty $prefs.profile 'avatar_index' $profile.Index
        Set-JsonProperty $prefs.profile 'using_default_name' $false
        Set-JsonProperty $prefs.profile 'using_default_avatar' $false
        Set-JsonProperty $prefs.profile 'using_gaia_avatar' $false
        if (-not $prefs.browser) { Set-JsonProperty $prefs 'browser' ([pscustomobject]@{}) }
        if (-not $prefs.browser.theme) { Set-JsonProperty $prefs.browser 'theme' ([pscustomobject]@{}) }
        Set-JsonProperty $prefs.browser.theme 'color_scheme2' 2
        if (-not $prefs.NewTabPage) { Set-JsonProperty $prefs 'NewTabPage' ([pscustomobject]@{}) }
        Set-JsonProperty $prefs.NewTabPage 'FooterVisible' $false
        Copy-Item $preferences "$preferences.bak" -Force
        Set-Content $preferences ($prefs | ConvertTo-Json -Depth 100) -Encoding UTF8

        if (-not $state.profile.info_cache.($profile.Directory)) {
            Set-JsonProperty $state.profile.info_cache $profile.Directory ([pscustomobject]@{})
        }
        $info = $state.profile.info_cache.($profile.Directory)
        Set-JsonProperty $info 'name' $profile.Name
        Set-JsonProperty $info 'avatar_icon' "chrome://theme/IDR_PROFILE_AVATAR_$($profile.Index)"
        Set-JsonProperty $info 'is_using_default_name' $false
        Set-JsonProperty $info 'is_using_default_avatar' $false
        Write-Host "Configured profile: $($profile.Name)"
    }
    Copy-Item $statePath "$statePath.bak" -Force
    Set-Content $statePath ($state | ConvertTo-Json -Depth 100) -Encoding UTF8
}

Fix the profile icons

I'm using 2 profiles on my browser: Company for business and Personal for private browsing. In Chrome I had custom profile pictures, but Brave does not have this much wanted feature.

Fortunately, there is a workaround. This script reads the current profile names and avatar slots, then prompts for an image URL for each profile. Leave a prompt empty to skip that profile. It will download the image and put it in the local avatar directory under the known avatar name, thereby overriding the image contents.

& {
    $ErrorActionPreference = 'Stop'

    function Save-Avatar($Name, $Profile) {
        $Prompt = "$($Profile.name) avatar URL (leave empty to skip)"
        $url = Read-Host $Prompt
        if ([string]::IsNullOrWhiteSpace($url)) { return }
        Invoke-WebRequest $url -OutFile "$avatarDirectory\$Name" -UseBasicParsing
    }

    $userData = "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data"
    $state = Get-Content "$userData\Local State" -Raw | ConvertFrom-Json
    $source = Invoke-WebRequest `
        'https://raw.githubusercontent.com/brave/brave-core/master/chromium_src/chrome/browser/profiles/profile_avatar_icon_util.cc' `
        -UseBasicParsing
    $avatars = @{}
    [regex]::Matches($source.Content, 'IDR_PROFILE_AVATAR_(\d+),\s*"([^"]+\.png)"') |
        ForEach-Object { $avatars["IDR_PROFILE_AVATAR_$($_.Groups[1].Value)"] = $_.Groups[2].Value }
    if (-not $avatars.Count) { throw 'Brave avatar mapping could not be read.' }

    $avatarDirectory = "$userData\Avatars"
    New-Item $avatarDirectory -ItemType Directory -Force | Out-Null

    foreach ($entry in $state.profile.info_cache.PSObject.Properties) {
        $info = $entry.Value
        $icon = $info.avatar_icon -replace '^chrome://theme/', ''
        if (-not $avatars.ContainsKey($icon)) {
            throw "No avatar file mapping for profile '$($info.name)' ($($info.avatar_icon))."
        }
        Save-Avatar $avatars[$icon] $info
    }

    Write-Host 'Profile images configured. Restart Brave to load them.'
}

And now you're greeted by:

Use custom profile icons in the Brave startup screen.
Bingo, profile pics solved!

Install the extensions

Finally, I want the same extensions in both profiles. Brave does not have a supported command-line switch for installing Web Store extensions, so this script uses a per-user normal_installed policy. The extensions are installed automatically in every Brave profile, but you can still disable them.

& {
    $ErrorActionPreference = 'Stop'
    if (!(whoami /groups | findstr S-1-16-12288)) {
        throw 'Run PowerShell as Administrator.'
    }
    $policyPath = 'HKCU:\SOFTWARE\Policies\BraveSoftware\Brave'

    $extensions = [ordered]@{
        Bitwarden      = 'nngceckbapebfimnlniiiahkandclblb'
        ColorZilla     = 'bhlhnicpbhignbdhedgjhgdocnmhomnp'
        MagicCSS       = 'ifhikkcafabcgolfjegfcgloomalapol'
        MarkdownViewer = 'ckkdlimhmcjmikdlpkmbgfkaikojcbjk'
        MinusUnsplash  = 'ancgmphhglnbebohehppcfjfdpakkcca'
        Momentum       = 'laookkfknpbbblfpciffpaejjkokdgca'
        SponsorBlock   = 'mnjggcdmjocbbbhaepdhchncahnbgone'
        WAVE           = 'jbbplnpkjmmeebjpijfedlgcdilocofh'
    }

    $settings = @{}
    foreach ($extension in $extensions.Values) {
        $settings[$extension] = @{
            installation_mode = 'normal_installed'
            update_url = 'https://clients2.google.com/service/update2/crx'
        }
    }

    New-Item $policyPath -Force | Out-Null
    New-ItemProperty $policyPath ExtensionSettings -PropertyType String `
        -Value ($settings | ConvertTo-Json -Depth 3 -Compress) -Force | Out-Null

    Write-Host 'Brave extension policy configured. Restart Brave to install the extensions.'
}

Restart Brave and check brave://policy to verify that the extension policy is active.

"Help" my muscle memory

I'm quick to do ctrl+v and type chrome. But if you want to run Brave (and you still have Chrome due to company policy), you have to patch it in the registry:

& {
  $ErrorActionPreference = 'Stop'
  $brave = @(
    "$env:ProgramFiles\BraveSoftware\Brave-Browser\Application\brave.exe"
    "${env:ProgramFiles(x86)}\BraveSoftware\Brave-Browser\Application\brave.exe"
    "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\Application\brave.exe"
  ) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1

  if (-not $brave) {
    throw 'Brave niet gevonden.'
  }

  $key = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe'

  New-Item $key -Force | Out-Null
  Set-Item $key -Value $brave
  Set-ItemProperty $key -Name 'Path' -Value (Split-Path $brave)

  $item = Get-Item $key

  Write-Host 'Registry patch gezet:'
  Write-Host "  chrome.exe -> $($item.GetValue(''))"
  Write-Host "  Path       -> $($item.GetValue('Path'))"
}