Calum MacRae /home/cmacrae

Managing Firefox on macOS with Nix

| 1286 words | nix darwin

In this post I'll talk about how I manage Firefox and mimick some of the behaviours/aesthetics I'd come to enjoy from using other browsers over the years.

Note: I'm in no way affiliated with Worldwide. It's a cool project and the site looks great, so figured I'd use it for a screenshot

Last winter, like many others, my experience with Chrome/Chromium had been a little sour for some time. I decided to make the jump to Firefox. Seeing as the browser was the one component of my system that I'd left out of my declarative configuration, I thought it'd be a good time to tackle that. The final piece.

Defining a package 📦

The Firefox package in nixpkgs has been broken for platforms.darwin for quite a while. I wasn't about to set out on trying to tackle that, as I simply didn't have the time. So instead, I wrapped up fetching the release .dmg in a simple overlay. Here's what it looks like currently:

{ stdenv, fetchurl, undmg }:

stdenv.mkDerivation rec {
  pname = "Firefox";
  version = "76.0.1";

  buildInputs = [ undmg ];
  sourceRoot = ".";
  phases = [ "unpackPhase" "installPhase" ];
  installPhase = ''
      mkdir -p "$out/Applications"
      cp -r "$out/Applications/"

  src = fetchurl {
    name = "Firefox-${version}.dmg";
    url = "${version}/mac/en-GB/Firefox%20${version}.dmg";
    sha256 = "1kvmyn375pb7mkcy410m8p1j550kv4g6dyx646igw68h1gz830va";

  meta = with stdenv.lib; {
    description = "The Firefox web browser";
    homepage = "";
    maintainers = [ maintainers.cmacrae ];
    platforms = platforms.darwin;

As you can see, it's a simple derivation that just fetches & unpacks the .dmg, then moves it to an Applications/ directory in the resulting $out. Using this overlay, you can simply add it to your user or system packages list and it'll end up in ~/.nix-profile/Applications/ or ~/Applications/

Tying it in with home-manager 🏡

The ever awesome rycee/home-manager has a really nice module for configuring Firefox. The above overlay can be configured as the package by simply setting programs.firefox.package = Firefox; (if Firefox is the name you bound the overlay to).

But the module lets you do much more than that.

Extensions from rycee's NUR repo 🔧

The programs.firefox module has an extensions option


List of Firefox add-on packages to install. Note, it is necessary to manually enable these extensions inside Firefox after the first installation.

Type: list of packages

So, if you can produce package derivations whos result represents a Firefox extension, you can add it to this list and they'll be installed. Fortunately rycee has a bunch available in his NUR expressions repo.

For anyone unfamilar with the NUR, here's an excerpt from the nix-community/NUR README

The Nix User Repository (NUR) is community-driven meta repository for Nix packages. It provides access to user repositories that contain package descriptions (Nix expressions) and allows you to install packages by referencing them via attributes. In contrast to Nixpkgs, packages are built from source and are not reviewed by any Nixpkgs member.

With this and rycee's firefox-addons packages (automatically generated & updated ✨) we have declarative management of extensions:

nixpkgs.config.packageOverrides = pkgs: {
  nur = import (builtins.fetchTarball "") {
    inherit pkgs;

home-manager.users.cmacrae.programs.firefox.extensions =
  with pkgs.nur.repos.rycee.firefox-addons; [

Fine grained configuration & mimicking Chrome profiles 👥

Another great feature of the programs.firefox module from home-manager: you can configure user profiles along with any settings available in about:config (stored in userprefs.js).

I was excited to see this, because my experience with Firefox's profile management so far had been lacklustre. I thought I could perhaps cook up something to mimick the profile management of Chrome. There are a few extensions out there for Firefox that offer session management based on profiles, but I really like the simplistic approach of having each instance/window of the browser run under a profile session. I'd gotten used to the pattern of having my work profile open on one workspace, and my personal profile open on another.

I've managed to achieve this fairly elegantly, I think. Here's what I've been using:

home-manager.users.cmacrae.programs.firefox.profiles =
  let defaultSettings = {
        "" = false;
        "browser.startup.homepage" = "";
        # ✂️- no need to splurge all my settings, you get the idea...
        "" = config.networking.hostName;
        "signon.rememberSignons" = false;
  in {
    home = {
      id = 0;
      settings = defaultSettings // {
        "browser.urlbar.placeholderName" = "DuckDuckGo";
        "toolkit.legacyUserProfileCustomizations.stylesheets" = true;
      userChrome = builtins.readFile ../conf.d/userChrome.css;

    work = {
      id = 1;
      settings = defaultSettings // {
        "browser.startup.homepage" = "about:blank";

With this, I'm setting up a bunch of common configuration I'd like to use for any profiles by let'ing defaultSettings. Then, for each profile I'm doing a merge operation with // (for those unfamilar, attributes on the right are merged over matching attributes on the left).

This yields a home profile with some styling (we'll get to that) and DuckDuckGo as the search engine, and a work profile whos only differing setting from the default configuration is an about:blank home page. I haven't taken the time yet to configure it, but it'd be dead simple to implement a different styled/coloured work profile so the browser windows differ in appearance, which would be nice.

To launch these different profiles in the manner I prefer, I have the following skhd bindings:

cmd + ctrl - f : open  -n ~/.nix-profile/Applications/ --args -P home
cmd + shift + ctrl - f : open -n ~/.nix-profile/Applications/ --args -P work

The firefox binary accepts a -P flag to set the profile. We can pass this using open's --args flag. This really nicely mimicks the behaviour of Chrome I mentioned. All browser data from each profile is kept entirely separate.

Changing appearance with userChrome 💅

And to go along with userprefs.js, the module also has a userChrome option. I only recently decided to take advantage of this. I've been using yabai for a while and have managed to configure my desktop appearance/functionality to closely resemble that of the tiling WMs I'd used on Linux/BSD over the years. Here's what it looks like as I'm writing

Intentionally overcrowded for the sake of demonstration

I used xombrero on OpenBSD for a number of years, and found myself longing for a somewhat similar appearance. Since I'd configured Alacritty and patched Emacs to drop their title bars to fit nicely with the nature of the environment, I figured it was probably possible with Firefox via userChrome too.

Some readers may have noticed the this line earlier in my profile expression

userChrome = builtins.readFile ../conf.d/userChrome.css;

I decided to break out the CSS for Firefox into its own file, as I knew how unwieldy it'd be. I've managed to conjure up something that hides all the elements I'm not interested in displaying, as I operate the browser mostly with the keyboard (thanks to Vimium).

Here's what it configures:

  • Hides window controls (macOS title bar)

  • Hides all "clutter" in the nav bar (including all extensions except browserpass)

  • Hide tabs if only one tab is open

  • Minimal appearance with a solarized-light inspired theme

  • Move the hamburger menu to the left of the URL bar

  • Move tabs inline to the right of the URL bar

Here's the full configuration for anyone who may want to use it. I've tried to keep it clean and well commented.

The home-manager module also supports userContent, but I'm yet to play around with that.

Closing words 👋

So with all this, my entire we browsing environment is declarative - just as the rest of my configuration. It's nice being able to set up on a new machine in a matter of minutes and have everything exactly as you like it - even all the intricacies.

This concludes the write-up. If you have any questions, please don't hesitate to comment or reach out to me on Twitter (@calumacrae). Until next time, happy hacking :)

Unless otherwise noted, the content of this site is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0).