# Customize a desktop (/docs/haus/desktops/customizing)



A desktop arrives already wired, so customising it is not assembly: it is turning
a handful of dials, and every one of them lives in the same file.

## The one file you edit [#the-one-file-you-edit]

**haus** (`github:hausfold/haus`) is the layer, and you never edit it to use it.
**Your config** (`~/.config/nix`) imports it and adds one host file,
`hosts/<hostname>/default.nix`, where everything on this page goes. It is a plain
nix-darwin module, so what you write merges with what haus declares.

```sh
haus edit       # opens it in $EDITOR
haus rebuild    # builds first; a broken config never activates
haus update     # a newer layer, without touching your identity, apps or overrides
```

## Finding the knob [#finding-the-knob]

* **Your config lists them.** `hosts/<host>/options.nix` arrives beside your host
  file with every `haus.*` option at its default, one sentence each, commented
  out. `haus options` regenerates it from the build you are running, writing
  `options.nix.new` rather than overwriting a copy you edited.
* **One line, no editor.** `haus set theme.accent teal` writes, type-checks and
  rebuilds in one go; `haus get` reads one back and
  [`haus reset`](/docs/haus/reference/haus#changing-a-setting-without-opening-an-editor)
  removes it.
* **Ask in words.** haus installs a skill that teaches a coding agent the whole
  surface: [Changing your Mac with an agent](/docs/haus/agent-rebuilds).

<Callout title="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.
</Callout>

## The cookbook [#the-cookbook]

The dials worth knowing by name. Each block is host-file Nix.

### Identity [#identity]

```nix
haus.git.name = "Ada Lovelace";
haus.git.email = "ada@example.com";
haus.git.signingKey = "6F7BD6F43A7C1420";  # "" disables commit signing
haus.git.org = "analytical-engine";        # the owner whose PRs fill ⌘G's tabs
```

`org` is where you work rather than who you are, and does nothing until
`haus.terminal.ghDash.enable = true` sits beside it, which in turn needs the Git
pack (`haus.developer.git.enable`) on: an assertion enforces it, because gh-dash
authenticates out of `gh`'s own credentials.

### The look [#the-look]

```nix
haus.theme.accent = "sapphire";      # any of the 14 Catppuccin accent names
haus.wallpaper.style = "minimal";    # none | minimal | orbits | constellation | flow | bold
haus.appearance.largePrint = true;   # bigger and sharper, whole machine, in one line
```

The accent reaches lazygit, fzf, yazi, Glow, the browser, the shelf and the
generated desktop, but not the terminal, and not the bar past its logo pill.
[Appearance](/docs/haus/rooms/appearance) has the palette and `largePrint`'s four
levers.

### Apps, windows, the bar and the launcher [#apps-windows-the-bar-and-the-launcher]

```nix
haus.roster.slack = { key = "s"; name = "Slack"; cask = "slack"; };
haus.terminal.editorName = "neovim";          # the ONE editor: installed, and $EDITOR
haus.terminal.hijackFileAssociations = false; # make that editor the default file opener
haus.windows.enable = true;    # false: no tiling, no Caps-Lock remap
haus.launcher.enable = true;   # false: ⌘Space stays Spotlight
haus.bar.enable = true;        # false: the native macOS menu bar stays
haus.bar.items.elgato = true;  # the optional pills are personal, so off unless you ask
```

The roster is the most common edit on the machine:
[Apps](/docs/haus/rooms/apps) has it, [the bar](/docs/haus/rooms/bar) its pills.

### Homebrew behaviour [#homebrew-behaviour]

```nix
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 rebuild
haus.homebrew.adopt = true;        # adopt an already-installed app instead of failing on it
```

<Callout type="warn" title="Two Homebrew edges">
  With `autoUpdate` and `upgrade` off a rebuild installs a cask once and leaves the
  version brew first laid down, which is how an app sits old on a fully synced
  machine. Move one with `brew upgrade --cask <name>`.

  `adopt` has the matching edge: an app already in `/Applications` is taken into
  Homebrew's bookkeeping rather than failing activation, and is then brew-managed,
  so dropping its roster entry on a `cleanup = "zap"` machine has the next rebuild
  zap an app you never installed through haus.
</Callout>

## Turning rooms off [#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.windows.enable`          | Tiling, the leader key; Caps Lock stays Caps Lock                                                                     |
| `haus.bar.enable`              | The custom bar; the native macOS menu bar comes back                                                                  |
| `haus.launcher.enable`         | The ⌘Space launcher                                                                                                   |
| `haus.shelf.enable`            | The notch file shelf; the copied `/Applications/Perch.app` stays until you delete it                                  |
| `haus.focus.enable`            | The quiet switch: bar pill, palette command and `focus` CLI                                                           |
| `haus.security.touchId.enable` | Touch ID for `sudo`, and back to the password prompt                                                                  |
| `haus.developer.enable`        | Language toolchains, Git tooling, the CLI toolbelt; `.git`, `.toolbelt` and `.languages` take out one piece at a time |
| `haus.ai.enable`               | The agent clients, scruff, and the lifecycle wiring around them                                                       |
| `haus.snippets.enable`         | Text expansion                                                                                                        |

The macOS defaults, the Homebrew policy and the `haus` CLI are the foundation the
rest stands on, and have no switch. Every room above is off in [the
foundation](/docs/haus/desktops/choosing#none-the-foundation), which is what
the installer selects.

Tiling **without** its keyboard claims is not the switch: `haus.keys.leader =
"none"` and `haus.keys.windowNav = "none"` keep the tiler arranging windows while
Caps Lock stays Caps Lock. The launcher has the same escape: only `cmd-space`
displaces Spotlight, so moving
[`haus.keys.palette`](/docs/haus/rooms/keys#not-fond-of-these-keys) to
`alt-space` or `ctrl-space` (or `"none"`) is how you keep both.

<Callout type="warn" title="the launcher doesn't hand ⌘Space back">
  haus disables Spotlight's shortcut with a one-way write and nothing re-enables
  it, so turning the **launcher** off later leaves ⌘Space where it is. Put it back
  in System Settings ▸ Keyboard ▸ Keyboard Shortcuts. (**focus** is the opposite:
  it *adds* a Do Not Disturb chord macOS ships disabled, so switching that room off
  leaves a spare binding rather than taking one away.)
</Callout>

## Disagreeing with your desktop [#disagreeing-with-your-desktop]

Your host file wins, with a **plain assignment**: no `lib.mkForce`, no import
order to reason about.

```nix
# the desktop set theme.accent = "mauve"; you'd rather have teal
haus.theme.accent = "teal";
haus.bar.enable = false;      # and no bar, thanks
```

Rooms declare defaults, a desktop states its choices above them, your host sits
above both, `haus set` writes above that. Switching desktops is one line in your
flake: [Choose a desktop](/docs/haus/desktops/choosing).

<Callout type="warn" title="Naming a list replaces it, so restate the parts you want">
  Your host wins **whole**. Name a list-typed option (`tour.steps`,
  `keys.leaderExtras`, `snippets.matches`) at all and you discard the desktop's
  version rather than adding to it, so write out every entry you want:

  ```nix
  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.
</Callout>

## Beating a default haus set [#beating-a-default-haus-set]

haus's Dock, Finder, trackpad and keyboard defaults are all *soft*: set the same
key plainly in your host file and yours wins. Anything under
[`system.defaults`](https://nix-darwin.github.io/nix-darwin/manual/) is fair game.

```nix
# 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";
```

<Callout type="warn" title="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 `mkDefault&#x60;s. Setting &#x2A;*`_HIHideMenuBar`** yourself gets you a
  *conflicting definition&#x2A; and a failed evaluation, which is the good outcome:
  forcing it while the bar is on puts two bars in one strip of pixels.
  &#x2A;*`SLSMenuBarUseBlurredAppearance`** (System Settings ▸ Menu Bar ▸ "Show menu bar
  background") haus rewrites on every activation, so a hand flip lasts until the
  next rebuild.

  Two more are nix-darwin's own. &#x2A;*`NSGlobalDomain.AppleInterfaceStyle`** macOS
  ignores outright: on macOS 26 and 27 the key is where macOS *mirrors* the appearance
  it's showing rather than a lever, so even a plist read-back agrees with you while
  nothing changes. Use `haus.theme.systemAppearance`; `haus diff&#x60; flags the other
  one. &#x2A;*`power.sleep.computer`** and its neighbours work, but not the way they
  read: they run `systemsetup`, which has no way to say *which* power source you
  mean, and one of them wrote the **charger** profile while the Mac was on
  **battery**. Use `haus.power.*`, which says `battery` and `charger` separately.
</Callout>

## Secrets [#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. `haus-secret --check`
fills the empty ones, and `haus.secrets.provider` decides where they are kept,
the login keychain by default, which is why they don't ride along to a second
machine. [Security](/docs/haus/rooms/security) has the providers.

<Callout title="Publishing your config? Put gitleaks in front of it.">
  A [gitleaks](https://github.com/gitleaks/gitleaks) pre-commit hook stops a stray
  key ever getting to *be* a commit. Scan the history you have **before** you flip
  the repo public: deleting a line later doesn't unpublish the key, so rotate
  anything it finds.

  ```sh
  cd ~/.config/nix
  printf '#!/bin/sh\nexec gitleaks git --pre-commit --staged --redact --verbose\n' \
    > .git/hooks/pre-commit
  chmod +x .git/hooks/pre-commit
  ```
</Callout>

## Options [#options]

[Every setting, with types and defaults](/docs/haus/reference/options).
