Safari draws its own toolbar icons, and most extension icons, as grey outlines that turn blue when the extension can act on the current page. uBlock Origin Lite does not follow that convention. Its shield is a filled maroon shape at all times, so it is the only coloured object in the toolbar. The extension's author has said he is open to a change, two people have sent icons, and the icon is still maroon. The tool described here fixes it locally, on one Mac, for one App Store build at a time. The interesting parts are why Safari tints some icons and not others, and what it takes to modify a signed App Store app without breaking it.
Summary. Safari treats any greyscale or single-colour toolbar icon as a template and paints it with the system accent colour on every tab the extension has access to, which for a content blocker is every tab, so a grey icon comes out blue. A small amount of colour in the icon's bar and border, measured at 45 units of channel spread, is enough to make Safari show the icon as drawn, and 24 units was not. The tool recolours the eight toolbar PNGs inside the extension with ImageMagick, re-signs the extension and the app with a free Apple Development certificate after removing the four entitlements that only validate for the original developer, and tells LaunchServices and pluginkit about the new bundle. Before touching anything it copies the Apple-signed app to ~/Library/Application Support, and it refuses to patch a bundle that is already modified. The App Store will not update a patched app, so the cycle is restore, update, patch again. The code is at github.com/marianposaceanu/ublock-iconizer.
The maroon shield and the fix that was reverted
The request is uBOL-home issue 402, opened on 5 August 2025. The reporter asked for colours that match Safari's standard, grey when inactive and blue when active, and attached a screenshot of a dark red icon among grey ones. The author, gorhill, replied that the extension sets no badge colour at all and leaves that to the browser, and that the Xcode project Apple's converter generated did not set a red colour either. A later comment linked an explanation of how Safari picks the colour. Safari applies the accent colour only to template images, which are black shapes on a transparent background, and shows anything else as drawn.
A contributor named uncw then made a greyscale version of the shield and pointed the Safari manifest at it, and a day later replaced it with an outlined version. gorhill pulled both commits into the main repository on 13 August 2025 and moved the image under the Safari platform directory so it would not ship to other browsers. Five days later he reverted all of it. The reason was the "no filtering" state. uBOL shows a different, greyed icon on sites where the user has switched filtering off, and Safari tinted that icon blue as well, so a site with filtering off looked exactly like a site with filtering on. He wrote that he could not release a Safari build with a toolbar icon that confusing.
The thread then went quiet for three months. In November 2025 a commenter explained that from Safari's point of view the tint is correct. The colour says whether the extension can act on the page, and the extension can, even when the user has told it not to filter. Extensions that need to show a second state do it with a different shape, and AdGuard's Safari extension was given as an example with separate on and off icons. In July 2026 a third contributor, brief, opened pull request 3953 against gorhill/uBlock along those lines, with an outlined "on" icon, a distinct "off" icon, and the code to switch between them. gorhill pulled the branch for testing and asked for a filled icon in the enabled state so the two states would differ more. The pull request was still open when this note was written.
For a local fix the situation is simpler. The App Store build is maroon, the shape of a proper fix is agreed, and nobody has finished it. A local patch does not need to solve the two-state problem, since the author of the patch can decide how much of the problem to accept, which is what the rest of this note does.
What Safari tints and what it leaves alone
The first attempt followed the advice in the thread and produced a black template icon. Safari tinted it blue on every page, since a content blocker has access to every page, and the result was a permanently blue shield. The goal was a neutral grey that stays grey, which meant finding out exactly what Safari counts as a template. The answer came from trying icons and looking at the toolbar after each one.
Safari treats an icon as a template when it is single-coloured or when every pixel is close to grey, and it shows the icon as drawn when the pixels carry enough colour. A pure grey shield was tinted. A warm grey whose red channel sat 10 units above the blue was tinted. A slate grey with 24 units between its highest and lowest channel was still tinted. A grey body with a very pale blue bar and border, where the blue channel of the detail colour sits 45 units above its red channel, was shown as drawn. Nothing between 24 and 45 was measured, so the threshold is somewhere in that range. A single-colour maroon icon, as a control, was tinted as well, which confirms that a single colour alone triggers the template treatment even when the colour is far from grey.
+---------------------------------+ +---------------------------------+
| Toolbar icon PNG |---->| Single colour, or every pixel |
+---------------------------------+ | within about 24 units of grey |
| +---------------------------------+
| |
| v
| +---------------------------------+
| | Treated as a template and |
| | painted accent blue on every |
| | tab the extension can access |
| +---------------------------------+
|
v
+---------------------------------+
| At least one colour 45 units |
| away from grey |
+---------------------------------+
|
v
+---------------------------------+
| Shown as drawn |
+---------------------------------+
The default style the tool applies, called grey, uses that finding. The shield body becomes a neutral #828282, the "no filtering" icons get a lighter #B4B4B4 body, and the white bar and border on both become #C8D8F5, a blue so pale it reads as white against the grey. The icon stays grey on an active tab and does not change colour with the accent setting. The cost is that the "on" and "off" states differ only in how dark the grey is, which is the same trade-off that stopped the upstream fix, accepted knowingly here. A second style, called template, produces the plain black icon for anyone who prefers the accent tint.
What the tool changes inside the app
uBlock Origin Lite for Safari is a thin macOS app that contains the web extension as an app extension bundle. The toolbar icons are ordinary PNG files inside that inner bundle, four sizes for the active state and four for the "no filtering" state. The tool rewrites those eight files and nothing else in the content of the bundle. The only other things that change are the two code signatures, which must be regenerated for the modified bundle to load at all.
/Applications/uBlock Origin Lite.app
+-- Contents/
+-- Info.plist read for the version
+-- _CodeSignature/ rewritten
+-- PlugIns/
+-- uBlock Origin Lite Extension.appex
+-- _CodeSignature/ rewritten
+-- Contents/Resources/img/
+-- icon_16.png recoloured, active state
+-- icon_32.png
+-- icon_64.png
+-- icon_128.png
+-- icon_16_off.png recoloured, "no filtering" state
+-- icon_32_off.png
+-- icon_64_off.png
+-- icon_128_off.png
The recolouring is two ImageMagick operations per file. For the grey style, pixels within 25 percent of white become the detail colour, and every other pixel becomes the body colour, with the operations restricted to the red, green and blue channels so the alpha channel is untouched. The anti-aliased edge of the shield survives, since the edge lives in the alpha channel and only the colours underneath it change. For the template style, pixels near white are made transparent, so the bar and border become holes, and the rest is painted black. Without the holes Safari's tint would turn the shield into a solid blob with no bar.
Detecting whether an icon has already been patched uses the same tool. ImageMagick reports the mean saturation of the opaque pixels in the HSL colour space. The stock maroon icon scores about 0.6, and both patched styles score under 0.1, so the status command can say whether the installed icons are stock or grey without keeping a record of its own.
Re-signing an App Store app with a free certificate
Changing a file inside a signed bundle breaks its seal, and macOS refuses to launch an app whose seal does not verify. Safari refuses to load an extension from one. The bundle has to be signed again, and the signature has to come from an identity macOS trusts. An Apple Development certificate is enough, and anyone with a free Apple ID can create one from the Accounts pane in Xcode's settings. Without such a certificate the tool signs ad hoc, which produces a bundle that launches but that Safari will only load after Allow Unsigned Extensions is switched on in the Develop menu, a setting that resets on every launch of Safari.
The entitlements are where a naive re-sign fails. An App Store app carries entitlements that bind it to the developer's team, and a certificate from a different team cannot sign for them. The tool reads the existing entitlements from the extension and from the app, drops the four team-bound ones, and signs each bundle with the rest under the hardened runtime, inner bundle first. The four removed are the application identifier, the team identifier, the keychain access groups and the application groups. The extension does not use the keychain or an app group for anything the user would notice, so the loss is not visible in practice.
Two things follow from the change of signing team. Safari sees the re-signed extension as a new extension, so it appears switched off in the Extensions pane and has to be enabled again, and every per-site permission has to be granted again. There is no way around that, since the permission records are keyed on the extension's identity. The other consequence is that LaunchServices and pluginkit, which keep the registry of app extensions, have to be told about the bundle. The tool runs lsregister -f on the app and pluginkit -a on the extension after signing, and again after restoring.
App Store installs are owned by root, and every update from the App Store reinstalls the app as root. Signing has to run as the user, since the certificate lives in the user's keychain, so the tool checks whether the bundle is writable and takes ownership with sudo chown when it is not. The password prompt comes back after every update for that reason.
The backup and the update cycle
The App Store will not update an app whose signature is not Apple's. A patched uBlock Origin Lite stops receiving updates, and since uBOL ships new filter list formats and bug fixes often, a patch with no way back would be a bad trade. The tool keeps a copy of the pristine app for exactly that purpose, and the rules around the copy are the most careful part of the code.
A backup is taken only when the installed app is signed by Apple and its seal still verifies. The copy is made with ditto, which preserves extended attributes, resource forks and the App Store receipt, into a directory named after the app's version under ~/Library/Application Support/ublock-iconizer. An existing backup that still verifies is never overwritten, since it may be the only pristine copy left. If the installed app is modified but its signature is broken, which is what a patch that failed halfway leaves behind, the tool refuses to proceed and points at the restore command. If the app is validly signed by someone else and the icons are not grey, it refuses as well, since it has no idea what else was changed.
+----------------------------------------+
| Check the app is Apple-signed and its |
| seal verifies |
+----------------------------------------+
|
v
+----------------------------------------+
| Copy the app with ditto into a |
| per-version backup |
+----------------------------------------+
|
v
+----------------------------------------+
| Recolour the eight toolbar PNGs with |
| ImageMagick |
+----------------------------------------+
|
v
+----------------------------------------+
| Re-sign the extension, then the app, |
| with portable entitlements |
+----------------------------------------+
|
v
+----------------------------------------+
| Register the bundle with |
| LaunchServices and pluginkit |
+----------------------------------------+
|
v
+----------------------------------------+
| Record the version, time, identity and |
| style in state.json |
+----------------------------------------+
Restoring reverses the swap. The backup is checked with codesign --verify first, and a backup that fails is refused with a suggestion to reinstall from the App Store. The verified copy is written next to the live app under a staging name, and only when the copy has finished is the live app removed and the staging copy renamed into place. A copy that fails halfway leaves the installed app as it was. After the swap the restored app verifies as Apple's, the registries are refreshed, and the App Store can update it again.
The update cycle is three steps. Run restore, let the App Store update the app, and run patch again. The patch command notices that the version has changed, takes a new pristine backup for the new version, and recolours the new icons. The status command compares the installed version with the one recorded at the last patch and says when the App Store has updated the app in the meantime.
Running it
Two things have to be installed that do not ship with macOS, ImageMagick 7 and the signing certificate. Ruby, codesign, plutil, ditto, pluginkit and lsregister are all part of the system, and the Ruby that Apple ships, version 2.6, is enough to run the script.
brew install imagemagick
security find-identity -v -p codesigning | grep "Apple Development"
git clone https://github.com/marianposaceanu/ublock-iconizer.git
cd ublock-iconizer
./ublock-iconizer status
./ublock-iconizer patch
./ublock-iconizer restore
After a patch or a restore, Safari has to be quit and reopened, and the extension enabled again in its Extensions pane. The status output on this Mac after the patch reads as follows.
App: /Applications/uBlock Origin Lite.app
Version: 2026.1103.1743
Signed by: Apple Development: (name and team id)
Icons: grey (patched, style grey)
Backup: ~/Library/Application Support/ublock-iconizer/
backup/2026.1103.1743/uBlock Origin Lite.app
Last patch: 2026-11-09T00:56:56+02:00 (version 2026.1103.1743)
How the code is laid out
The tool started as one 400-line script and is now a thin runner that loads a library directory with one class per file. The runner reads the arguments and hands them to a command line class, which picks one of three command classes. The command classes are the only place where the order of operations lives, and they are written as a list of named steps so that a reader can follow a patch from top to bottom without opening any other file.
ublock-iconizer runner: require the library, run the CLI
lib/ublock_iconizer/
+-- cli.rb arguments to options and a command class
+-- commands/
| +-- base.rb app exists, take ownership
| +-- status.rb version, signer, icon state, backup
| +-- patch.rb readiness, backup, recolour, sign
| +-- restore.rb swap the verified backup back in
+-- app_bundle.rb paths, version, signing authority, pristine
+-- icon_painter.rb grey and paint, through magick
+-- code_signer.rb sign, verify, default identity
+-- backup_store.rb save and restore, one copy per version
+-- state.rb state.json
+-- style.rb the two styles and their colours
+-- shell.rb run commands, raise on failure
The one decision worth pointing out is in the patch command. Before it changes anything, a single method looks at the app and answers one of four words, pristine, already patched, invalid signature, or foreign signer, and the command acts on that word. The checks that produce the answer have no side effects, so the refusal paths can be read on their own, and a new condition would be one more line in that method rather than another guard scattered through the flow.
Where this leaves things
The icon on this Mac is grey, the App Store build is one command away, and the patch takes a few seconds to apply again after an update. The upstream fix is still the right one, and the open pull request against gorhill/uBlock is where the icon will eventually be decided. Until it lands, the tool at github.com/marianposaceanu/ublock-iconizer does the job for one person at a time, and its README lists what was tested and what was not.
Sources
- ublock-iconizer, the Ruby tool described here
- uBOL-home, issue 402: icon colour on Safari's toolbar does not match the standard colours
- gorhill/uBlock, the 18 August 2025 revert of the Safari toolbar icon
- gorhill/uBlock, pull request 3953: add Safari toolbar icons, open at the time of writing
- Jeff Johnson, on Safari extension toolbar icons and template images
- Apple, Hardened Runtime
- ImageMagick, command-line options: fuzz, opaque, transparent, colorize