Customize a desktop
One file holds every choice you make: the dials worth knowing, the rooms you can switch off, and how to beat anything your desktop set.
A desktop ships a whole opinion, already wired. So customising it isn't assembly: it's turning a handful of dials, and every one of them lives in the same file.
The one file you edit
There are two halves to a haus machine, and they never mix:
- haus (
github:hausfold/haus): the layer. Generic, no identity, upstream. You never edit it to use it. - Your config (
~/.config/nix): a small flake that imports haus, plus one host file,hosts/<hostname>/default.nix, where you live.
Everything on this page goes in that host file:
haus edit # opens it in $EDITOR
haus rebuild # builds first; a broken config never activatesIt's a plain nix-darwin module, so whatever you write merges with what haus
already declares. Because it's a separate repo, haus update pulls a newer
layer without touching your identity, your apps or your overrides.
Finding the knob
Three ways in, and none of them is reading a reference end to end:
- Your own config already lists them. A fresh install drops a
hosts/<host>/options.nixbeside your host file: everyhaus.*option at its default, one sentence each, all commented out. Uncomment what you want.haus optionsregenerates it from the build you're actually running, so it can't advertise something your machine hasn't got. If you've edited your copy it writesoptions.nix.newbeside it rather than overwriting your work. - One line, no editor.
haus set theme.accent tealwrites the setting, type-checks it and rebuilds in one go;haus getreads one back andhaus resetremoves it. The haus CLI reference has the model. - Ask for it in words. haus installs a skill that teaches a coding agent this whole surface, generated from the revision you run; see Changing your Mac with an agent.
Scaffolded, not active
Nothing in options.nix does anything until you uncomment a line and import
it; haus options prints the imports = [ ./options.nix ]; line to add if it's
missing. Commented-out is deliberate, and for two reasons: a file that spelled
every default out would pin every one of them against your desktop, and it would
freeze haus's own defaults against ever being retuned upstream. Uncomment what
you actually mean.
The cookbook
The tour of what's worth knowing. Each block is host-file Nix.
Identity
haus.git.name = "Ada Lovelace";
haus.git.email = "ada@example.com";
haus.git.signingKey = "6F7BD6F43A7C1420"; # "" disables commit signing
haus.git.org = "analytical-engine"; # the GitHub owner your work lives underorg is the odd one out: not who you are, but where you work. gh-dash reads it
to fill ⌘G's PR tabs with that owner's open, green, red and just-shipped work,
so an org rename is one word rather than one per tab. On its own it does
nothing: set haus.hearth.ghDash.enable = true alongside it, and keep the Git
pack on (haus.developer.git.enable); that's what installs gh, and turning
it off while the overlay is on stops the build.
The look
haus.theme.accent = "sapphire"; # any of the 14 Catppuccin accent names
haus.wallpaper.style = "minimal"; # none | minimal | orbits | constellation | flow | boldThe accent recolours lazygit, fzf, yazi, Glow, the browser and the generated
desktop together, but not the terminal, and not the bar beyond its logo
pill. Single-file dotfiles that bake a palette at their own theme slot (Ghostty,
starship, tmux, bat, zellij) keep their built-in colour on purpose, and a flake
check fails the build if one of them ever starts following. minimal is the
desktop haus generates for you (a flat field in your
palette, the ⌂ mark at its centre, a bloom in your accent), and "none" keeps
the wallpaper you already have. The Appearance room has the
whole palette and the desktop's own dials.
The terminal
haus.hearth.editorName = "neovim"; # the ONE editor: installed, and $EDITOR
haus.hearth.hijackFileAssociations = false; # make that editor the default file openerMore in the Development room.
Apps, windows and the launcher
haus.roster.slack = { key = "s"; name = "Slack"; cask = "slack"; };
haus.prowl.enable = true; # false: no tiling, no Caps-Lock remap
haus.pounce.enable = true; # false: ⌘Space stays Spotlight
haus.pounce.signingIdentity = # sign it, and its Accessibility grant survives rebuilds
"Developer ID Application: Ada Lovelace (ABCDE12345)";The roster is the most common edit on the machine and has a page of its own.
The bar
haus.sill.enable = true; # false: the native macOS menu bar stays
haus.sill.items.elgato = true; # opt-in extras, see The barThe optional pills are personal (an Elgato key-light toggle, a Harvest timer, an agents readout), so they're off unless you ask. The bar lists them.
Homebrew behaviour
haus.homebrew.cleanup = "none"; # none | uninstall | zap: what a rebuild does to undeclared apps
haus.homebrew.autoUpdate = false; # brew update before each rebuild
haus.homebrew.upgrade = false; # upgrade outdated packages each rebuildcleanup is "none" so nothing you installed by hand is ever removed; "zap"
gets you a fully declarative machine where an undeclared cask is uninstalled on
the next rebuild.
A rebuild keeps a cask installed, not current
autoUpdate and upgrade are off so a rebuild is reproducible: it installs a
cask once, then leaves whatever version brew first laid down. That's why an app
can sit on an old version on a fully synced machine. Upgrade one on demand with
brew upgrade --cask <name>, or set both to true and accept that two rebuilds
days apart can land different app versions.
Turning rooms off
Every room you can see has a switch, and turning one off leaves the rest working:
Set this to false | And you lose |
|---|---|
haus.prowl.enable | Tiling, the leader key; Caps Lock stays Caps Lock |
haus.sill.enable | The custom bar; the native macOS menu bar comes back |
haus.pounce.enable | The ⌘Space launcher |
haus.perch.enable | The notch file shelf; the copied /Applications/Perch.app stays until you delete it |
haus.hush.enable | The quiet switch: bar pill, palette command and hush CLI |
haus.collar.enable | Touch ID for sudo, and back to the password prompt |
haus.developer.enable | The Development room: language toolchains, the Git tooling, the CLI toolbelt. Sub-switches (.git, .toolbelt, .languages) take out one piece at a time |
haus.ai.enable | The AI room: the agent clients, holt, and the lifecycle wiring around them |
haus.snippets.enable | Text expansion |
haus.apps.videoPlayer.enable | IINA; videos keep whatever handler they have |
The macOS defaults, the Homebrew policy and the haus CLI itself are the
foundation the rest stands on, and have no switch. Every room you can see
does, and each one is off in the blank
desktop, so what's on your Mac is whatever your
desktop asked for plus whatever you did.
pounce doesn't hand ⌘Space back
Turning pounce off later doesn't return ⌘Space to Spotlight: haus disables Spotlight's shortcut with a one-way write, and nothing re-enables it. Put it back yourself in System Settings ▸ Keyboard ▸ Keyboard Shortcuts. (hush is the opposite case: it adds a Do Not Disturb chord macOS ships disabled, so switching the room off leaves a spare binding rather than taking one away.)
If what you want is tiling without its keyboard claims, don't switch the
room off: set haus.keys.leader = "none" and haus.keys.windowNav = "none"
instead, and prowl keeps arranging windows while Caps Lock stays Caps Lock. The
launcher has the same escape: haus.keys.palette takes alt-space,
ctrl-space or "none", and since only cmd-space displaces Spotlight, moving
it is how you keep both. See
rebinding.
Disagreeing with your desktop
Your host file wins. Anything the desktop chose, you change with a plain
assignment (no lib.mkForce, no import order to reason about):
# the desktop set theme.accent = "mauve"; you'd rather have teal
haus.theme.accent = "teal";
haus.sill.enable = false; # and no bar, thanksThat's the ladder doing its job: rooms declare defaults, a desktop states its
choices above them, your host sits above both, and anything haus set writes
sits above that. Switching desktops is one line in your flake; see
Choose a desktop.
Naming a list replaces it, so restate the parts you want
Your host wins whole. A list-typed option (tour.steps,
keys.leaderExtras, snippets.matches) that you name at all discards the
desktop's version rather than adding to it, so if you want most of what the
desktop had plus one of your own, write out all of them:
haus.snippets.matches = [
{ trigger = "@@"; replace = "ada@example.com"; } # the desktop's, kept
{ trigger = ";sig"; replace = "— Ada"; } # and mine
];To take a list away entirely, use its room's switch: haus.tour.enable = false rather than an empty tour.steps, which is a type error.
Bigger type
haus.appearance.largePrint = true is one named answer to a whole-machine
question, and it pulls two complementary levers. haus.ui.scale moves haus's own
type together (the terminal, the palette, the bar, the Dock, Finder's sidebar,
the tiling gaps), and haus.displays.main.uiScale moves the display one step
toward larger text, which enlarges everything including apps haus knows nothing
about. They multiply, which is the intended effect and a bigger jump than
either alone. hausdisp list prints the persistent UUIDs if you want to name
one display rather than "main".
One limit is macOS's rather than haus's: the menu bar's height. There is no
setting for it, so the bar's type follows ui.scale only so far and then stops.
Making the whole bar bigger is what display scaling is for: it changes what a
point means, which is the only thing that band responds to.
Beating a default haus set
haus sets Dock, Finder, trackpad and keyboard defaults, and every one is a soft default: set the same key plainly in your host file and yours wins.
# haus hides the Dock and puts it on the bottom. Put it on the left, always shown:
system.defaults.dock.autohide = false;
system.defaults.dock.orientation = "left";Anything under system.defaults
is fair game.
Four keys that don't play by that rule
Two menu-bar keys track whether the bar is on, so haus sets them plainly rather
than as mkDefaults. _HIHideMenuBar is the auto-hide: set it
yourself and you get a conflicting definition and a failed evaluation, not a
quiet win. That is the good outcome, because forcing it while the bar is on
puts two bars in the same strip of pixels. SLSMenuBarUseBlurredAppearance
(System Settings ▸ Menu Bar ▸ "Show menu bar background") haus rewrites on every
activation, because Tahoe's Liquid Glass made the hidden menu bar's hover-reveal
see-through; flip it by hand and the next rebuild flips it back.
One macOS ignores outright: NSGlobalDomain.AppleInterfaceStyle (light or
dark). It is a real nix-darwin option, and on macOS 26 writing it does nothing:
the key is where macOS mirrors the appearance it's showing, not a lever, so
even a plist read-back agrees with you while nothing changes. Use
haus.theme.systemAppearance; haus diff flags the other one.
And one that works, but not the way it reads: nix-darwin's own
power.sleep.computer and its neighbours run systemsetup, which has no
way to say which power source you mean: on macOS 26 one of them wrote the
charger profile while the Mac was on battery, and nothing said so. Use
haus.power.*, which says battery and charger separately.
Secrets
Your config is text in a git repo, so secret values never go in it. haus declares which secrets exist and fetches them at runtime through secretspec:
secretspec check # what's declared and still missing
secretspec set <NAME> # fill one inBy default they live in this Mac's login keychain, which is why they don't ride
along when you clone the config onto a second machine. Point
haus.secrets.provider at a cloud vault if you'd rather they followed you.
Publishing your config? Put gitleaks in front of it.
If you'd rather share the repo than keep it private, add a gitleaks pre-commit hook so a stray key never gets to be a commit, and scan the history you already have before you flip it public. Deleting a line later doesn't unpublish the key, so rotate anything it finds.
cd ~/.config/nix
printf '#!/bin/sh\nexec gitleaks git --pre-commit --staged --redact --verbose\n' \
> .git/hooks/pre-commit
chmod +x .git/hooks/pre-commitStart blank
The from-scratch desktop: no optional rooms, no claimed hotkeys, nothing that changes how the Mac behaves. Then add rooms one at a time.
Create a desktop
A desktop is one file whose only top-level key is haus. The closed format, what it may and may not set, and how to prove it before anyone else runs it.