# nebelhaus — complete documentation Every page of https://nebelhaus.com, in sidebar order. The index with per-page descriptions is at https://nebelhaus.com/llms.txt. This documents the LATEST rice. A machine is pinned to a revision and may not have the newest options — `haus status` says how far behind it is, and an installed machine carries its own generated reference at ~/.claude/skills/nebelhaus/references/options.md. --- # What is nebelhaus? Source: https://nebelhaus.com/start/what-is-nebelhaus/ An opinionated macOS rice — silver-grey, keyboard-first, reproducible, and Nix-native. What it is, who it's for, and what you actually get. **nebelhaus** turns a Mac into a quiet, keyboard-first, fog-grey workstation — arranged like a tiling Linux rig, but native to the grain of the Mac. It is a small family of tools tied together by one Nix flake. Run one command and the whole house rises: a tiling window manager, a status bar, a themed shell, a command palette, Touch-ID sudo, and a single muted colour scheme painted across every app you own. Wipe the machine and rebuild it, and it stands again — bar, windows, theme, and all — because the entire system is described in text and pinned to exact commits. ## The pitch, in one breath > An opinionated macOS, raised in the fog. Silver-grey · keyboard-first · > reproducible · nix-native. Grey is the point. The default palette — [nebelung](/reference/palette/) — is a muted dark theme for people who find most themes too loud, named for a cat breed the colour of high fog. It also comes in a **light** mode and a **high-contrast** one, if soft grey isn't what your eyes want. ## Who it's for - You like **tiling window managers** and keyboard-driven workflows, and wish the Mac felt more like one without fighting it. - You want your machine to be **reproducible** — describable in git, restorable on a fresh laptop in one command. - You appreciate a **calm, cohesive look** across the terminal, editor, git, and browser instead of a different accent colour in every app. - You're comfortable in a terminal. You do **not** need to know Nix going in — the installer scaffolds everything — but you'll get more out of it over time. If you've never touched Nix, that's fine. [Nix](https://nixos.org) is what makes the setup reproducible: the whole machine is described in text files, and one command makes reality match them. ## What you get | Piece | Codename | What it does | |---|---|---| | Tiling & launcher | **prowl** | [AeroSpace](https://github.com/nikitabobko/AeroSpace) tiling, driven from the home row; tap Caps-Lock to launch or throw windows | | Status bar | **sill** | [SketchyBar](https://github.com/FelixKratz/SketchyBar) with workspace pills, weather, media, battery, clock | | Shell & terminal | **hearth** | zsh + starship + [Ghostty](https://ghostty.org) + zellij + yazi + helix, all themed | | Touch-ID sudo | **collar** | fingerprint auth for `sudo` — even inside a terminal multiplexer | | Command palette | **pounce** | a native ⌘Space launcher where every command is a file | | Messages client | **trill** | a native iMessage/SMS/RCS window, reading `chat.db` read-only | | File shelf | **perch** | a native tray that grows out of the notch to catch drag-drops and fling them out as a group | | The colours | **nebelung** | one silver-mist palette rendered onto 20+ tools | | System core | **den** | macOS defaults, Homebrew policy, the `haus` CLI | Each piece also stands on its own — you can take the whole rice, or just [pounce](/guides/pounce/), or just [the palette](/reference/palette/). ## The house holds your hand An opinionated system only works if you're never stranded in it. nebelhaus is built so there's always a hand to grab: - **Forgot a key?** Tap then / — the live cheatsheet, generated from *your* roster, is one tap away. (Or the [written one](/reference/keybindings/).) - **Something feels off?** `haus doctor` checks Nix, the agents, and the permissions in one shot, and tells you the exact command to unstick each. - **Made a mess?** `haus rollback` returns to the previous generation atomically. A failed build never activates in the first place. - **Nothing is destructive.** The installer snapshots before it starts, backs up any dotfile it touches, and never deletes an app you installed — walking away is as supported as moving in. - **The docs meet you where you are** — from the [first ten minutes](/start/first-run/) to [every option](/reference/options/) to [the exact fix](/reference/troubleshooting/) for the five things that ever go wrong. ## The two-command install ```sh # 1. Scaffold your config (installs Nix if needed, interviews you, writes files) curl -fsSL https://nebelhaus.com/init.sh | bash # 2. Build and switch into it cd ~/.config/nix nix build .#darwinConfigurations.$(scutil --get LocalHostName).system \ && sudo ./result/sw/bin/darwin-rebuild switch --flake . ``` The [Install](/start/install/) page walks through exactly what the one-liner does — and how to read it first if you'd rather not pipe curl into bash blind. ## How the pieces relate The rice is **generic, complete, and identity-free**. Your name, keys, secrets, and private app list live in a thin config you own at `~/.config/nix` — never edited into the rice itself. The rice stays upstream, where `nix flake update` pulls new versions. See [The family](/start/the-family/) and [How the flakes fit together](/internals/flakes/) for the full picture. Ready? Head to [Install](/start/install/). --- # Install Source: https://nebelhaus.com/start/install/ The one-liner, exactly what it does, and how to run it safely — from a fresh Mac to a fully built system. The entire install is one line: ```sh curl -fsSL https://nebelhaus.com/init.sh | bash ``` That scaffolds your config and installs Nix if you don't have it. It does **not** change your system on its own — it writes files and then tells you the one command that activates them. Nothing is switched until you run that command. ## What you need first - A **Mac** running a recent macOS (Apple Silicon only — Intel Macs aren't supported). - **Xcode Command Line Tools.** The installer will trigger the install prompt if they're missing, then ask you to re-run. - About **10–15 minutes** and a few GB of download for the first build. **No Nix? No problem.** You do not need Nix installed beforehand. The bootstrap installs [Determinate Nix](https://docs.determinate.systems/) for you. If you already run stock Nix, the installer will stop and explain — nebelhaus expects Determinate. ## Reading it first (recommended) `curl … | bash` runs whatever the URL serves. `nebelhaus.com/init.sh` only **proxies** the rice's [`bootstrap.sh`](https://github.com/nebelhaus/nebelhaus/blob/main/bootstrap.sh) from this org's own repo — it's boring and readable on purpose. To inspect it before running: ```sh # print it instead of running it curl -fsSL https://nebelhaus.com/init.sh | less # or pin an exact release tag and diff it against GitHub curl -fsSL 'https://nebelhaus.com/init.sh?ref=v2026.07.18' | less ``` By default the one-liner serves the **latest release tag**; before the first release it falls back to `main`. Releases are date-tagged, so add `?ref=v2026.07.18` to pin a specific day's version. ## What the bootstrap does 1. **Preflight.** Confirms you're on macOS, reads your username and hostname, and takes a [Time Machine local snapshot](https://support.apple.com/en-ca/guide/mac-help/mh35836/mac) as a coarse rewind point (non-fatal if Time Machine is off). 2. **Prerequisites.** Checks for Xcode CLT (prompts if missing) and installs Determinate Nix if `/nix` isn't already set up. It refuses to run on top of a non-Determinate Nix install rather than risk breaking it. 3. **Interview.** A short [gum](https://github.com/charmbracelet/gum) TUI asks for your **git name and email**, a **preset** (Full rice / Minimal / Custom — which decides the sill, prowl, and pounce rooms), your **accent colour** (default `mauve`), a **wallpaper** (default: keep yours), your **`$EDITOR`** (default `hx`) — and whether to **keep your current macOS settings** (Dock/keyboard/Finder) and **adopt your existing Homebrew casks** into the new config. Every answer has an environment-variable override for non-interactive runs (see below). 4. **Preflight audit.** A read-only report of what the first `switch` will change: existing Homebrew casks (**none are removed**), dotfiles that will be backed up, and the macOS settings that will change (Dock autohide, key repeat, Caps-Lock remap if prowl is on, ⌘Space reassignment if pounce is on). 5. **Scaffold.** Creates `~/.config/nix/` with a thin `flake.nix` that imports `github:nebelhaus/nebelhaus`, plus a `hosts//default.nix` holding your identity and choices. It `git init`s and commits the result. Nothing is activated yet. The bootstrap ends by printing the build command. ## Build and switch ```sh cd ~/.config/nix nix build .#darwinConfigurations.$(scutil --get LocalHostName).system \ && sudo ./result/sw/bin/darwin-rebuild switch --flake . ``` The `build` runs first; a failed build never touches your system. Once it succeeds, `switch` activates everything: apps install, background agents start, dotfiles land, and the `haus` command joins your `PATH`. First run takes a while — it's downloading the world. Subsequent rebuilds are fast. Then head to [First run](/start/first-run/) to learn the muscle memory. ## Non-interactive / scripted installs Every interview answer has an environment-variable override, so you can run the whole thing unattended: ```sh NEBELHAUS_NONINTERACTIVE=1 \ NEBELHAUS_GIT_NAME="Ada Lovelace" \ NEBELHAUS_GIT_EMAIL="ada@example.com" \ NEBELHAUS_ROOMS="sill,prowl,pounce" \ NEBELHAUS_ACCENT="mauve" \ NEBELHAUS_WALLPAPER="none" \ NEBELHAUS_EDITOR="hx" \ bash -c "$(curl -fsSL https://nebelhaus.com/init.sh)" ``` Other useful knobs: | Variable / flag | Effect | |---|---| | `NEBELHAUS_DRY_RUN=1` | Print the generated config instead of writing it | | `NEBELHAUS_NONINTERACTIVE=1` or `--defaults` | Skip the interview, accept all defaults | | `NEBELHAUS_KEEP` | Which current macOS settings to keep (`dock,keyboard,finder`) | | `NEBELHAUS_DIR` | Scaffold somewhere other than `~/.config/nix` | | `--from ` / `NEBELHAUS_FROM` | **Restore** an existing config instead of scaffolding — see [Moving to a new Mac](/guides/new-mac/) | **Your undo cards** Two things can roll you back: the **APFS snapshot** taken at step 1 restores macOS settings, and `sudo darwin-rebuild --rollback` atomically returns to the previous generation. Homebrew apps are **never** deleted by the rice — remove them yourself with `brew uninstall --zap ` if you want to. ## Installing with an agent You can hand most of this to an AI agent. The site publishes a machine-readable index at **[nebelhaus.com/llms.txt](https://nebelhaus.com/llms.txt)** (and the whole corpus at [llms-full.txt](https://nebelhaus.com/llms-full.txt)), so any tool that can fetch a URL can read the real documentation instead of guessing from whatever it remembers about Nix. Paste something like this: ```text Read https://nebelhaus.com/llms.txt and then the install page it links to. I want to install nebelhaus on this Mac. Do the parts you can: run the bootstrap with NEBELHAUS_DRY_RUN=1 first and show me the config it would write, then run it for real, then run the `nix build`. Stop and hand back to me for anything needing sudo — I'll run those myself. ``` **What an agent can and can't do here**, which is worth being explicit about: | Step | Who | |---|---| | Read the docs, choose options, dry-run the bootstrap | agent | | Run the bootstrap (scaffold `~/.config/nix`) | agent | | Review the generated host file with you | agent | | `nix build .#darwinConfigurations..system` | agent | | Install Determinate Nix · the first `darwin-rebuild switch` | **you** | The last row is the real boundary: both need `sudo`, and Touch ID for sudo ([collar](/guides/touch-id/)) doesn't exist until *after* that first switch — so they want an interactive password prompt an agent pane can't answer. Everything up to "the build succeeded" is fair game, and a failed build activates nothing. **After the first switch it gets easier** Once the house stands, the rice installs a Claude Code skill whose option reference is generated from the exact revision you're pinned to — so "install Slack" or "make everything bigger" becomes an edit plus `haus rebuild`, with no docs to fetch. See [Changing your Mac with an agent](/guides/ai-agent/). Prefer the non-interactive variables above when an agent drives the bootstrap: the interview is a [gum](https://github.com/charmbracelet/gum) TUI, and a tool driving it blind is how you end up with someone else's git identity in your config. ## Just want one piece? You don't have to take the whole rice: **pounce (Homebrew)** ```sh brew tap nebelhaus/tap brew install pounce brew services start pounce pounce --request-accessibility ``` **pounce (Nix)** ```sh nix run github:nebelhaus/pounce -- --help ``` **nebelung (colours)** Add the flake and pull rendered themes for any tool: ```nix inputs.nebelung.url = "github:nebelhaus/nebelung"; # then reference ${nebelung.packages.${system}.default}//... ``` See [pounce](/guides/pounce/) and [the palette](/reference/palette/) for the standalone stories. --- # First run Source: https://nebelhaus.com/start/first-run/ Your first ten minutes after switching in — the muscle memory that makes nebelhaus click. You've run the [install](/start/install/) and the first `darwin-rebuild switch` has finished. Here's what changed and what to try first. ## What just happened - **AeroSpace** (tiling) is running. Windows now arrange themselves. - **SketchyBar** replaced the menu bar with workspace pills, weather, media, battery, and a clock. - **Ghostty** is your terminal; opening it drops you straight into a **zellij** session, themed in fog-grey. - **Pounce** is listening on **⌘Space**. - The **`haus`** command is on your `PATH`. ## The four moves to learn first **⌘Space — Pounce** Open the command palette. Type to fuzzy-search apps and commands, hit Return. Start here for everything. **Tap ⇪ — Launch mode** Tap (don't hold) Caps-Lock, then press an app's letter to launch or focus it. T = terminal, B = browser out of the box. **Tap ⇪, then ←↓↑→ — Focus** Tap Caps-Lock, then an arrow to move focus between tiled windows — keep arrowing to keep moving. Hold with an arrow to *move* the focused window instead. No Vim keys to learn. **Tap ⇪, then 1–4 — Workspaces** Tap Caps-Lock, then a digit to jump to that workspace — the same leader that launches an app. Add to the digit (or to an app's letter) to throw the current window there instead. **The cheatsheet is built in** Forgot a binding? Tap then / to open Pounce's live cheatsheet — every shortcut, generated from your actual app roster. The full list also lives at [Keybindings](/reference/keybindings/). ## Try this sequence 1. **Tap ⇪, then `T`** — Ghostty opens and tiles. You're in zellij. 2. **`⌘Space`, type `saf`, Return** — Safari launches. That's Pounce. 3. **Tap ⇪, then ← / →** — focus jumps between the two windows (keep arrowing). 4. **⌥/** — the tiled split flips between horizontal and vertical. (`⌥,` swaps to an accordion instead.) 5. **`⌘Space`, type `emoji`, Return** — the palette swaps to an emoji grid in place. Pick one; it's copied (and pasted, if you granted Accessibility). ## Grant Accessibility to Pounce (one-time) Some Pounce features (clipboard auto-paste, emoji paste) synthesize keystrokes, which macOS gates behind Accessibility: ```sh pounce --request-accessibility # approve the system dialog pounce --check-accessibility # prints true when it's granted ``` If you set `nebelhaus.pounce.signingIdentity` in your host file, this grant **survives rebuilds** — the rice re-signs a stable copy of the app so macOS keeps trusting it. Without it, the palette still works; only the paste-back features are off. See [Pounce config](/reference/pounce/). ## Make it yours Your identity and app list live in `~/.config/nix/hosts//default.nix`. Open it fast with: ```sh haus edit ``` A few first tweaks people reach for: ```nix { # Change the accent that tints lazygit, fzf, yazi, and the browser nebelhaus.theme.accent = "sapphire"; # Prefer a different editor nebelhaus.hearth.editor = "nvim"; # Add an app to the launcher + workspace roster nebelhaus.apps.slack = { key = "s"; name = "Slack"; workspace = "S"; appId = "com.tinyspeck.slackmacgap"; barIcon = ":slack:"; cask = "slack"; }; # Or just install casks without giving them a launcher key homebrew.casks = [ "discord" ]; } ``` Then apply it: ```sh haus rebuild ``` See [Theming & accents](/guides/theming/), [Window management](/guides/window-management/), and the full [options reference](/reference/options/) to go deeper. ## Daily driving from here | Command | What it does | |---|---| | `haus rebuild` | Build, then switch — your everyday apply | | `haus update` | Pull new nebelhaus versions, then rebuild | | `haus status` | Current generation + how stale the rice is | | `haus rollback` | Return to the previous generation | | `haus doctor` | Health check: Nix, CLT, GUI agents, cask drift | The full list is on the [haus CLI](/reference/haus/) page. **You** Lost a keybinding? /. Something misbehaving? `haus doctor` names the culprit and the fix. Regret a change? `haus rollback`. The [troubleshooting page](/reference/troubleshooting/) covers everything else. --- # The family Source: https://nebelhaus.com/start/the-family/ The repos that make up nebelhaus, what each one owns, and how they feed each other. nebelhaus is not one repo — it's a small family, each piece owning one job and usable on its own. The rice ([nebelhaus](https://github.com/nebelhaus/nebelhaus)) ties them together, but pounce, trill, perch, and nebelung stand alone too. ## The repos | Repo | Owns | You touch it when… | |---|---|---| | 🏠 [**nebelhaus**](https://github.com/nebelhaus/nebelhaus) | the rice — nix-darwin modules for tiling, bar, shell, security, palette wiring | anything about *how the system behaves* | | 🐾 [**pounce**](https://github.com/nebelhaus/pounce) | the launcher — a native Swift command palette + its generic command scripts | the palette app or a built-in command changes | | 🐦 [**trill**](https://github.com/nebelhaus/trill) | the messages — a native iMessage/SMS/RCS client reading `chat.db` read-only | you want to change the Messages client | | 🐈 [**perch**](https://github.com/nebelhaus/perch) | the shelf — a native notch file tray that stages dropped files and drags them out as one group | you want to change the file shelf | | 🌫 [**nebelung**](https://github.com/nebelhaus/nebelung) | the colours — a silver-mist Catppuccin variant + per-tool theme templates | you want a different shade of fog | | 🍺 [**homebrew-tap**](https://github.com/nebelhaus/homebrew-tap) | the Homebrew tap (`brew tap nebelhaus/tap`) | almost never — CI bumps it on every release | | 🧰 [**workshop**](https://github.com/nebelhaus/workshop) | every repo checked out side-by-side + the `bench` dev CLI | you're hacking on the family itself | Your private machine config — identity, secrets, your app roster — lives at `~/.config/nix` and is **yours**, never part of the rice. ## The CLIs at a glance There are four command-line tools in nebelhaus, and knowing which is which saves a lot of confusion. **Two jobs, four commands** — and if you only *use* nebelhaus, you only ever need the first one: | Command | You reach for it to… | Works on | Ships in | |---|---|---|---| | **`haus`** | drive your own machine — [rebuild, update, roll back, diagnose](/reference/haus/) | your Mac (`~/.config/nix`) | the rice — every install has it | | **`wt`** | manage [agent worktrees](/guides/claude-agents/) — safe parallel agents, resumable panes | any git repo | the rice — every install has it | | **`bench`** | [move a change across the family repos](/internals/contributing/) — try, ship, release | the workshop checkouts | the workshop — contributors only | | **`zscratch`** | [feel-test a zellij edit](/internals/contributing/#feel-testing-a-zellij-edit-zscratch) with no rebuild | the rice's zellij config | the rice — for rice contributors | `haus` and `bench` never overlap — they're named differently on purpose so they can't shadow each other. `haus` knows only *your machine*; `bench` knows only *the family repos*. `wt` and `zscratch` are dev tools the rice puts on your `PATH` whether or not you ever contribute — `wt` especially is worth knowing for **anyone** who runs Claude Code, nebelhaus contributor or not. ## The rooms, standalone - **[pounce](/guides/pounce/)** installs from Homebrew (`brew install pounce`) with zero Nix. It's a general command palette; the rice just wires it up. - **[perch](https://github.com/nebelhaus/perch)** is a plain menu-bar app — the notch file shelf — that needs none of the rest of the family to run. - **[nebelung](/reference/palette/)** is a plain colour system. Any tool — Ghostty, bat, lazygit, Slack, Zen — can consume its rendered themes without touching the rest. - **[nebelhaus](/start/what-is-nebelhaus/)** is the whole system, but its modules (`den`, `hearth`, `prowl`, `sill`, `collar`, `pounce`, `hush`, `secrets`) are exported individually so you can cherry-pick. ## How they feed each other The repos form a chain of pinned flake inputs. Each link is a *flake input* — a reference to an exact commit, recorded in the downstream repo's `flake.lock`: ![one colour change rippling down the chain: nebelung → pounce → nebelhaus → ~/.config/nix → your Mac, each lock pinning the exact commit of the one before](/media/ripple.webp?v=2) The catch that surprises everyone: a flake input is **not** "whatever is on GitHub right now" — it's a frozen commit hash. So pushing a change to `nebelung` changes **nothing** downstream until each downstream `flake.lock` is bumped to the new commit. That's what makes rebuilds reproducible, and it's why updating is a deliberate `nix flake update`, not automatic. As an end user you rarely feel this — `haus update` handles it. It matters if you [contribute to the family](/internals/contributing/) or want to understand [how the flakes fit together](/internals/flakes/). ## Built on nebelhaus stands on the shoulders of excellent open-source projects: - [Nix](https://nixos.org) & [nix-darwin](https://github.com/nix-darwin/nix-darwin) — reproducible system config - [AeroSpace](https://github.com/nikitabobko/AeroSpace) — the tiling window manager - [SketchyBar](https://github.com/FelixKratz/SketchyBar) — the status bar - [Catppuccin](https://github.com/catppuccin) — the colour framework nebelung is derived from - [Ghostty](https://ghostty.org), [zellij](https://zellij.dev), [yazi](https://yazi-rs.github.io), [helix](https://helix-editor.com), [starship](https://starship.rs) — the terminal stack All of nebelhaus is MIT-licensed. --- # Making it yours Source: https://nebelhaus.com/guides/making-it-yours/ The one file you edit, the opinions worth knowing, and a cookbook of every knob — from accents to workspaces to turning whole rooms off. nebelhaus ships a *complete* opinion — system and shell, already wired. So "customizing" isn't reassembling anything; it's turning a handful of dials. This page is the map of those dials and the thinking behind the loudest defaults. ## The one file you edit There are two halves to your setup, and they stay cleanly apart: - **The rice** (`github:nebelhaus/nebelhaus`) — the generic, no-identity system. You *never* edit this to use it. It updates upstream; you pull it when you like. - **Your config** (`~/.config/nix`) — a ~18-line flake that imports the rice, plus **one host file** where *you* live: `hosts//default.nix`. That host file is where every tweak on this page goes. Open it instantly: ```sh haus edit ``` It's a plain nix-darwin module — whatever you write merges with what the rice already declares. Change something, then: ```sh haus rebuild # builds first; a broken config never activates ``` **Why the split matters** Because *your* stuff and *the rice* are separate repos, `nix flake update nebelhaus` (or `haus update`) pulls new fog without ever touching your identity, apps, or overrides. Your machine stays yours; the rice stays upstream. Keep personal things in your host file — never in the rice repo itself. ## The one-app-per-workspace idea The single biggest opinion in nebelhaus is how windows are arranged, so it's worth saying out loud: **each app you live in gets its own workspace, and one key takes you there.** Instead of alt-tabbing through a soup of windows, you build muscle memory: the terminal is always T, the browser is always B, Slack is always S. The app is launched if it isn't running, focused if it is, and it lands on *its* workspace every time. Your hands stop negotiating with the window manager. That whole scheme comes from **one keyed map** — `nebelhaus.apps`, the roster — so the launcher key, the workspace, the bar pill, and the auto-tiling rule can never drift out of sync. Keyed entries also compose cleanly across your host file and pounce-generated package modules. A few things worth internalizing: - **Letters are apps, digits are scratch.** Workspaces `1`–`4` are generic scratch space; letters (`T`, `B`, `S`…) belong to specific apps. - **Focus vs. throw.** Tap then a letter/digit to *go there*. Same leader, but with that letter/digit, to *throw the current window* there instead. - **Launcher-only apps exist.** Give a roster entry `workspace = null` and its key just launches it in place — no dedicated workspace, no pill. Handy for a password manager or a calculator. Adding your own is a one-liner — see [Adding apps & tools](/guides/adding-apps/) for the recipe and [Window management](/guides/window-management/) for the full binding set. ## The cookbook Every knob below is a `nebelhaus.*` option (or a plain nix-darwin one) you set in your host file. This is the tour; the exhaustive list with types and defaults is the [options reference](/reference/options/). Or don't look any of it up. The rice installs a skill that teaches Claude Code this whole surface — generated from the revision you're pinned to — so you can ask in plain words instead: [Changing your Mac with an agent](/guides/ai-agent/). ### Identity ```nix nebelhaus.git.name = "Ada Lovelace"; nebelhaus.git.email = "ada@example.com"; nebelhaus.git.signingKey = "6F7BD6F43A7C1420"; # "" to disable commit signing ``` ### The look ```nix nebelhaus.theme.accent = "sapphire"; # any of 14 Catppuccin accent names nebelhaus.theme.wallpaper = "orbits"; # none | orbits | constellation | flow | bold ``` `accent` recolours lazygit, fzf, yazi, and the browser accent; `bold` generates a wallpaper from your accent. Full palette on [Theming & accents](/guides/theming/). ### The shell ```nix nebelhaus.hearth.editor = "nvim"; # the ONE editor: $EDITOR + every "open in editor" action nebelhaus.hearth.hijackFileAssociations = false; # make that editor the default file opener ``` More on each in [The shell](/guides/the-shell/). ### Apps, tiling & the launcher ```nix nebelhaus.apps.slack = { /* one keyed roster entry — see Adding apps */ }; nebelhaus.prowl.enable = true; # false: no tiling, no Caps-Lock remap nebelhaus.pounce.enable = true; # false: no ⌘Space palette nebelhaus.pounce.signingIdentity = ""; # keeps Pounce's Accessibility grant across rebuilds ``` ### The bar ```nix nebelhaus.sill.enable = true; # false: keep the native macOS menu bar nebelhaus.sill.plugins = [ "elgato" ]; # opt-in extras: agents | elgato | harvest ``` The bar's optional plugins are personal — an Elgato Key Light toggle, a Harvest timer, an agents pill — off unless you ask. See [The bar](/guides/the-bar/). ### The first-run tour The built-in tour teaches nebelhaus's four core moves. A shared rice can replace that lap with its own instructions while staying data-only: ```nix nebelhaus.tour.steps = [ { hint = "Press ⌘Space, type tour, then hit ↵"; detect = "palette"; } { hint = "Move to another workspace"; detect = "workspace"; } ]; ``` Steps run in order and advance from signals the rice already emits — `launch`, `workspace`, `navigate`, `resize`, or `palette` — rather than by logging keys. The first four need prowl; `palette` needs Pounce. A left-click skips the current step, and the module warns during a rebuild if a step depends on a room you disabled. Leave `tour.steps` at its default `null` to keep the built-in tour. ### Homebrew behaviour ```nix nebelhaus.homebrew.cleanup = "none"; # none | uninstall | zap — how rebuild treats undeclared apps nebelhaus.homebrew.autoUpdate = false; # brew update before each rebuild nebelhaus.homebrew.upgrade = false; # upgrade outdated packages each rebuild homebrew.casks = [ "discord" ]; # plain installs, no launcher key ``` `cleanup` defaults to `"none"` so nothing you installed by hand is ever removed. Set it to `"zap"` if you want a *fully* declarative machine where undeclared casks get uninstalled on rebuild. **Keeping casks current (e.g. trill)** `autoUpdate` and `upgrade` are **off by default** so a rebuild is reproducible — it installs a cask once, then pins whatever version brew first laid down. That's why a cask app can sit on an old version even though your config is fully synced: a rebuild keeps it *installed*, not *current*. To pick up new releases of a date-versioned family app like [trill](/guides/trill/), either upgrade it on demand — ```sh brew upgrade --cask trill ``` — or set both knobs to `true` so every rebuild tracks upstream latest: ```nix nebelhaus.homebrew.autoUpdate = true; # brew update first, so it sees new versions nebelhaus.homebrew.upgrade = true; # then upgrade outdated casks ``` The trade-off is the flip side of reproducibility: a rebuild may now upgrade **all** your casks at once, so two rebuilds days apart can land different app versions. Prefer per-host: set these in your host file, not the rice default, so the rest of the family keeps deterministic rebuilds. ## Turning rooms off Three rooms are opt-*out*; the rest are load-bearing: | Room | Option | Off means | |---|---|---| | prowl (tiling) | `nebelhaus.prowl.enable = false` | No AeroSpace, no leader key | | sill (bar) | `nebelhaus.sill.enable = false` | The native menu bar stays | | pounce (launcher) | `nebelhaus.pounce.enable = false` | ⌘Space stays Spotlight | If what you actually want is the tiler **without** its keyboard claims, don't turn the room off — set `nebelhaus.keys.leader = "none"` and `nebelhaus.keys.windowNav = "none"` instead. prowl keeps tiling and Caps-Lock stays Caps-Lock. See [rebinding](/guides/window-management/#not-fond-of-these-keys). **den** (system + Homebrew), **hearth** (shell), and **collar** (Touch ID sudo) are always on — they're the foundation the rest sits on. Turn any of the three optional rooms off and the others keep working; the rice degrades gracefully. ## Presets: start from someone else's rice A **preset** is a data-only rice — a file whose only top-level key is `nebelhaus`. No `pkgs`, no `lib`, no activation scripts, so it can't reach outside the option surface and you can read one in a minute and know the worst it can do. Import one alongside your host file: ```nix darwinConfigurations.myhost = nebelhaus.mkNebelhaus { username = "ada"; hostname = "myhost"; host = ./hosts/myhost; extraModules = [ nebelhaus.presets.everyday ]; }; ``` Your host file is imported *after*, so anything you set there still wins. | Preset | For | |---|---| | `full` | the whole rice: bar, tiling, palette, developer pack. The default. | | `minimal` | just the themed shell — no bar, no tiling, no palette. Still a developer machine. | | `everyday` | the inverse: a Mac for someone who doesn't write code. No developer pack, no Caps-Lock remap. | | `large-print` | bigger and sharper — the rice UI and whole display, plus the high-contrast palette and macOS's own contrast lift. | The first three are whole rices; pick one. **`large-print` is a layer** — it describes seeing rather than the person, so it stacks: ```nix extraModules = [ nebelhaus.presets.everyday nebelhaus.presets.large-print ]; ``` It uses two complementary size controls. `ui.scale = 1.4` moves the terminal font, the Dock icons and the tiling gaps together — pin any single one afterwards and yours wins. `displays.main.uiScale = "larger-text"` moves the current main display one step toward larger text, which makes **everything** bigger, including third-party apps the rice knows nothing about. The trade-off is less desktop space. For per-display control, add a UUID-specific setting in your host file; it wins over the preset's broad `main` selector. `hausdisp list` prints the persistent UUIDs of the displays currently attached. The remaining size limits are the menu bar and the Pounce window: both use geometry tuned to their layouts, so a multiplier would break alignment rather than enlarge them. The preset also doesn't change an app's own font preference or macOS's separate Dynamic Type text-size setting; display scaling is the system-wide lever that works. Want to make one for other people? [Sharing a rice](/guides/sharing-a-rice/) covers the data-only boundary, `checkRice`, composition testing, and the smallest useful publishing shape. ## Overriding macOS defaults The rice sets Dock, Finder, trackpad, and keyboard defaults — but every one of them is a *soft* default you can beat by just setting the same key with a plain value in your host file: ```nix # Rice 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`](https://nix-darwin.github.io/nix-darwin/manual/) is fair game. Your plain value wins over the rice's default. **Two exceptions** Both menu-bar keys track whether the bar (sill) is on, and both override your value rather than yielding to it: - **`_HIHideMenuBar`** — the auto-hide. Set it by hand and you'll get a bar and a menu bar fighting for the same strip of pixels. - **`NSGlobalDomain.SLSMenuBarUseBlurredAppearance`** — System Settings ▸ Menu Bar ▸ "Show menu bar background". Tahoe's Liquid Glass made the hidden menu bar's hover-reveal translucent, so Sill showed straight through it; the rice forces it opaque again. Flip it in System Settings and the next rebuild flips it back. ## Secrets & private things Anything sensitive — API keys, tokens, your app list if you'd rather it stay private — belongs in your host file (which is *your* repo), and secrets themselves belong in files the Nix store never sees: ```nix home-manager.users.${username} = { # Read secrets at shell startup, not from the (world-readable) Nix store: programs.zsh.initContent = lib.mkAfter '' export SOME_API_KEY="$(cat ~/.secrets/some-key 2>/dev/null)" ''; home.packages = with pkgs; [ /* your private tools */ ]; }; ``` Keep `~/.secrets/*` out of git. Your host repo can be private; the rice never needs to know who you are. **Publishing it instead? Put gitleaks in front** If you'd rather share your host repo than keep it private, add a [gitleaks](https://github.com/gitleaks/gitleaks) pre-commit hook so a stray key never gets to *be* a commit: ```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 ``` Add `gitleaks` to `home.packages` so the hook has something to run. It exits non-zero on a hit and stops the commit; `git commit --no-verify` overrides it when you're sure. Scan the history you already have *before* you flip the repo public — plain `gitleaks git` walks every commit, and deleting the line later doesn't unpublish the key, so rotate anything it finds. **Wrong repo?** Changing a *color*? That's the [nebelung](/reference/palette/) palette, a different repo. Changing the rice's *shared* behaviour for everyone (not just your machine)? That's the nebelhaus repo — see [Contributing](/internals/contributing/). Personalizing *your* Mac? You're in the right place: your host file. ## Where to go next - [Adding apps & tools](/guides/adding-apps/) — the most common change, in depth. - [Moving to a new Mac](/guides/new-mac/) — because all of this is just git. - [Keeping in sync](/guides/staying-in-sync/) — updating, rolling back, `haus`. - [Options reference](/reference/options/) — every knob, formally. --- # Sharing a rice Source: https://nebelhaus.com/guides/sharing-a-rice/ Author, validate, and publish a data-only nebelhaus rice that other people can inspect, compose, and trust. A shared rice is an ordinary `.nix` file that changes only `nebelhaus.*`. Keeping that boundary small makes the result easy to inspect, combine with other rices, and override from a host file. ## The format The file evaluates to an attribute set whose only top-level key is `nebelhaus`: ```nix { nebelhaus = { developer.enable = false; prowl.enable = false; keys.leader = "none"; theme = { flavor = "latte"; accent = "rosewater"; }; sill.items.weather = false; tour.steps = [ { hint = "Press ⌘Space, type tour, then hit ↵"; detect = "palette"; } ]; }; } ``` It is not a module function: there is no `{ pkgs, lib, config, ... }:` header. It also does not set `system.defaults`, install packages, or add activation scripts. Every setting stays inside the documented [`nebelhaus.*` option surface](/reference/options/). **Whole rice or layer?** A **whole rice** describes a kind of machine or person and usually chooses all the big rooms: developer tools, tiling, the bar, and the launcher. A **layer** describes one concern, such as larger text, and sets only what that concern needs. Layers are easier to compose because they leave unrelated choices alone. ## Validate the boundary Run the same structural check as nebelhaus's built-in presets: ```sh nix eval --impure --expr \ '(builtins.getFlake "github:nebelhaus/nebelhaus").lib.checkRice ./writer.nix' ``` It prints `true`, or throws with the top-level key that escaped the data-only boundary. `checkRice` is the trust-boundary check; it does not prove that every option name or value is valid. For that, import the rice into a real host: ```nix extraModules = [ ./rices/writer.nix ]; ``` Then evaluate the configuration before activating it: ```sh nix eval .#darwinConfigurations.myhost.system.drvPath ``` If that succeeds, `haus rebuild` still gives you the usual build-before-apply safety. ## Test composition, not just isolation A useful rice should survive being combined with a layer and then overridden by its user: ```nix extraModules = [ ./rices/writer.nix nebelhaus.presets.large-print ]; ``` Before publishing, try: - the rice by itself; - the rice plus an unrelated layer; - a host value overriding one of the rice's choices; - each combination of rooms you turn off. If the result depends on an exact module order, narrow the rice until ordinary Nix merging is enough. Later modules can win conflicts, but order should not be the rice's hidden organizing principle. ## Publish something inspectable The smallest useful repository is deliberately boring: ```text writer-rice/ ├── README.md ├── writer.nix └── LICENSE ``` In the README, say: - who the rice is for and what it changes; - whether it is a whole rice or a layer; - which nebelhaus revision you tested; - how to import it; - what it deliberately does not handle, including required rooms or permissions; - whether both `checkRice` and a real host evaluation passed. For a first release, invite people to review and vendor the `.nix` file. A remote flake wrapper can come later if versioned imports become useful; it should not hide the data users are being asked to trust. ## What does not belong Keep these in a private host file or an explicitly more powerful module: - identity, secrets, tokens, device UUIDs, and personal account names; - a personal app roster that reveals more than the rice needs; - raw macOS defaults outside `nebelhaus.*`; - package or store paths that require `pkgs`; - activation hooks and shell commands. A configuration that genuinely needs `pkgs`, scripts, or arbitrary nix-darwin options is a **power module**. That can be useful, but it carries a different trust model and should be labelled as one. ## Before you share - [ ] The file has only a top-level `nebelhaus` key. - [ ] `checkRice` prints `true`. - [ ] A real host configuration evaluates. - [ ] The rice composes with an unrelated layer. - [ ] Host overrides still win. - [ ] Disabled rooms agree with dependent features, including [`tour.steps`](/guides/making-it-yours/#the-first-run-tour). - [ ] The README states the audience, scope, tested revision, and limits. - [ ] There is no identity, secret, or device-specific data. --- # Changing your Mac with an agent Source: https://nebelhaus.com/guides/ai-agent/ Your machine is declarative, so an AI agent can reconfigure it safely — the nebelhaus skill, the build-then-switch loop, and the one thing it will refuse to do. Ask an agent to set up a normal Mac and it will `brew install` things, write dotfiles, and click through System Settings on your behalf. On a nebelhaus machine most of that would be silently undone by the next rebuild — and you'd have no record of what changed. Declarative config flips that. Every change is text in one file, `haus rebuild` **builds before it switches** so a broken edit never reaches the running system, and `haus rollback` undoes an applied one atomically. That makes yours the rare machine an agent can reconfigure without it being reckless. The missing half was knowledge. So the rice ships it. ## What's installed Every rebuild writes a Claude Code **skill** to `~/.claude/skills/nebelhaus`: | File | What it is | |---|---| | `SKILL.md` | The loop, the boundaries, the traps. | | `references/options.md` | Every `nebelhaus.*` option — **generated from the exact rice revision you're pinned to**. | | `references/this-machine.md` | Your host: hostname, host-file path, which rooms are on, your theme and keys, your app roster and its taken leader keys. | | `references/recipes.md` | Worked examples for the common asks. | | `consumer-CLAUDE.md` | A starter `CLAUDE.md` for your config repo (see below). | The option reference being *generated* is the point. A hand-written cheatsheet goes stale the first time an option is added, and a confidently-wrong option name costs you a failed build. This one can only ever describe options that exist on your revision — and `haus update` regenerates it along with the rice. **Check it** `haus doctor` has an **Agents** section that reports whether the skill is installed, whether your config repo has a `CLAUDE.md`, and whether the app you're running from can write macOS's protected settings. ## The loop it follows 1. **Orient** — read `this-machine.md` and your host file, and check `haus status`. 2. **Find the option** — grep the generated reference. If it isn't listed, it doesn't exist on your revision, and the agent should say so rather than guess. 3. **Edit `~/.config/nix/hosts//default.nix`** — the one file you own. 4. **Apply with `haus rebuild`** — the build gates the switch. 5. **Verify, or `haus rollback`.** ## What to ask for Ordinary requests, in your own words: - *"Install Slack and give it its own workspace"* → an entry in [`nebelhaus.apps`](/reference/options/#nebelhausapps), with the leader key, the workspace, the bar pill and the Homebrew cask all following from it. - *"Everything's too small"* → [`nebelhaus.ui.scale`](/reference/options/#nebelhausuiscale). - *"Switch to light mode"* → [`nebelhaus.theme.flavor`](/reference/options/#nebelhausthemeflavor). - *"Hide the weather in the bar"* → the matching `nebelhaus.sill.items.*` toggle. - *"Bind a key to open my notes"* → `nebelhaus.keys.leaderExtras`. The skill tells the agent to prefer a `nebelhaus.*` option over a raw nix-darwin setting, and to say so when nothing covers what you asked — because that's a gap in the rice, not something to bolt onto your machine. ## What it won't do - **Touch identity or secrets** without asking — git identity, signing keys, `nebelhaus.secrets.*`. - **Edit the rice, `flake.lock`, or anything in `/nix/store`.** Pulling a newer rice is `haus update`, not a hand edit. - **Uninstall apps behind your back.** Removing an app from your config stops nebelhaus *managing* it; the app stays on disk until you `brew uninstall --zap` it. ## The one rebuild it will refuse If your host file sets `system.defaults.universalaccess.*` directly, `haus rebuild` refuses to run from an agent session that can't write it, and tells you to run the same command yourself. That domain is TCC-protected: macOS only lets an app holding **Full Disk Access** write it, and the grant belongs to the app your session runs under — a terminal you've granted it to, versus a desktop app or a cloud session that you haven't. nix-darwin emits that write unguarded into an activation script running under `set -e`, so a refusal aborts activation partway and skips everything after it, including every background service the rice installs. Your bar, tiling and palette come back dead, with the symptom nowhere near the cause. Refusing is the kinder failure. Nothing is changed, and your edit is still on disk. **Reaching those settings safely** [`nebelhaus.accessibility.*`](/reference/options/#nebelhausaccessibility) reaches the useful keys in that domain with a **guarded** write: without the grant you lose the setting and nothing else. For contrast specifically, [`nebelhaus.theme.contrast`](/reference/options/#nebelhausthemecontrast) is the rice's own palette axis and always applies. ## What a rollback doesn't undo `haus rollback` rewinds everything Nix manages, instantly. It does **not** rewind macOS system settings a rebuild wrote (Dock, keyboard, Finder), and it does not rewind Homebrew casks — those live outside Nix generations entirely. A good agent says which kind of change it's about to make. ## A CLAUDE.md for your config `~/.config/nix` is your own git repo — your machine in text. Dropping a `CLAUDE.md` in it orients any agent you open there, whether or not it found the skill. A starter ships inside the skill; nothing writes it for you, because it's your repo and you should own the file: ```sh cp ~/.claude/skills/nebelhaus/consumer-CLAUDE.md ~/.config/nix/CLAUDE.md ``` ## Other tools, and before you've installed The skill is Claude Code's format, and it only exists on a machine that has already switched in at least once. For everything else — a different AI tool, or someone asking about nebelhaus before they own it — this site publishes itself in machine-readable form: - **[nebelhaus.com/llms.txt](https://nebelhaus.com/llms.txt)** — a table of contents with one-line descriptions, for a tool that fetches selectively. - **[nebelhaus.com/llms-full.txt](https://nebelhaus.com/llms-full.txt)** — every page inlined, for one that would rather read the lot. Both describe the **latest** rice. On an installed machine, prefer the skill: it's generated from the revision you actually have. For installing in the first place, see [Installing with an agent](/start/install/#installing-with-an-agent). ## Turning it off ```nix nebelhaus.claude.skill = false; ``` A machine that never runs an agent otherwise just carries an unread markdown file — the option exists so `~/.claude/skills` stays yours if you'd rather manage it by hand. **It** An agent that can rebuild your Mac can change how your Mac behaves. The safety here is real but it is *recovery*, not prevention: the build gates the switch and the rollback is atomic, so mistakes are cheap — they aren't impossible. Read the diff before you say yes, exactly as you would for any other change to your config. --- # Adding apps & tools Source: https://nebelhaus.com/guides/adding-apps/ The whole decision tree — a GUI app with its own launcher key, a plain cask, or a CLI tool from nixpkgs, and where each one belongs. Sooner or later you'll want Slack, or Discord, or `ripgrep`, or that one obscure CLI you saw on a blog. Good news: you almost never touch the rice to do it. The quickest route is **Install App** in pounce: pick a missing app from the rice's curated **Popular apps** shelf or search a package source, choose whether the app belongs in your launcher roster or should only be installed, and pounce writes a small Nix module plus rebuilds. It is declarative, not a hidden imperative install. Prefer to write it yourself? Edit your host config and rebuild: ```sh haus edit # opens ~/.config/nix/hosts//default.nix # …add your app or tool… haus rebuild # build, then switch ``` Everything you declare there *merges* with what the rice and pounce-generated package modules already declare. The only question is **which option** to use. **One Nix model, two doors** Manual entries and **Install App** use the same options. Pounce writes one file per selection under `hosts/<hostname>/packages/`; `mkNebelhaus` auto-imports that directory. You can inspect, edit, move, or delete those files like any other Nix module—there is no separate `roster.json`. Or let Pounce make the edit: open **Install App** and choose **Popular apps** or where to search (Homebrew, the Mac App Store, or the flake's pinned Nixpkgs). The curated shelf hides apps already present on the Mac and does not expose which package source backs each pick. Each choice is written as a small Nix module under `hosts//packages/` and applied with `haus rebuild`; a Homebrew cask or App Store app can also be added to the tiling roster, including its launcher key and optional workspace. For a workspace app, Pounce checks the pinned [SketchyBar App Font mapping](https://github.com/kvndrsslr/sketchybar-app-font) and writes the verified `barIcon` ligature too. If upstream has no match, the pill keeps its workspace letter rather than guessing an icon. **Why App Store installs open a terminal** Pounce runs `mas get` visibly before any roster rebuild. App Store downloads can ask for an Apple Account, Touch ID, or sudo, and hiding that inside `darwin-rebuild` can look like a hung activation. If `mas` cannot acquire the app, Pounce opens its App Store page instead. There's nothing to set up first — no data files, no options to point at. Each selection lands as its own Nix module under `hosts//packages/`, and Pounce stages and commits just that file after a successful rebuild. ## Which line do I add? **A GUI app I** Want it tiled, with a Caps-Lock letter and its own workspace? Add it to the **app roster** — one entry gives you the key, the workspace, the bar pill, and (optionally) installs it. **A GUI app, no fuss** Just want it *installed* — no launcher key, no tiling opinions? One line in **`homebrew.casks`**. **A CLI tool I** A command you want on your `PATH` for good? Add it to **`home.packages`** from nixpkgs — and it rolls back with everything else. **A CLI tool, just once** Trying something out? Run it **without installing** via `,` (comma). Nothing to clean up. --- ## A GUI app with a launcher key (the roster) This is the nebelhaus-native way to add an app you actually work in. One keyed entry in `nebelhaus.apps` wires up *everything at once*: a Caps-Lock letter to launch/focus it, a workspace it lives on, a pill in the bar, the auto-tiling rule, a row in the cheatsheet — and it installs the app for you. ```nix nebelhaus.apps.slack = { key = "s"; name = "Slack"; workspace = "S"; appId = "com.tinyspeck.slackmacgap"; barIcon = ":slack:"; label = "Slack"; cask = "slack"; }; ``` Rebuild, and tapping then S launches Slack onto workspace `S`, every time. The two fields people always have to look up: - **`appId`** — the app's bundle id, used to auto-send its windows to the right workspace. Find it once the app exists on disk: ```sh osascript -e 'id of app "Slack"' # → com.tinyspeck.slackmacgap ``` - **`barIcon`** — a ligature from the [SketchyBar app font](https://github.com/kvndrsslr/sketchybar-app-font) (e.g. `:slack:`, `:discord:`, `:figma:`). Browse the list on that page; leave it `null` and the bar just shows the workspace letter. **cask = install, null = already here** Set `cask = "slack"` and declaring the app **also installs it** — no separate line needed. Set `cask = null` for apps that are already on the machine (Safari, Music, anything from the App Store) but you still want a launcher key for. The full field-by-field breakdown — and the one-app-per-workspace thinking behind it — lives in [Window management](/guides/window-management/#your-app-roster) and [Making it yours](/guides/making-it-yours/#the-one-app-per-workspace-idea). **It may theme itself on the way in** If the name you key the entry under matches a tool nebelung has a theme for — `zed`, `warp`, `xcode`, `rio`, `gitui`, `kitty` and a few dozen more — the Nebelung theme file lands where that app looks for themes on the next rebuild, in your flavor, contrast and accent. Some apps pick it up on their own; others need one click you'll find waiting in `haus doctor`. See [Theming ▸ Apps you added yourself](/guides/theming/#apps-you-added-yourself). ## A GUI app, no launcher key (a plain cask) Some apps you just want *installed* — a background utility, something you open from Spotlight-muscle-memory, a thing you touch once a month. Skip the roster and drop the [Homebrew cask](https://formulae.brew.sh/cask/) straight in: ```nix homebrew.casks = [ "discord" "obsidian" "figma" ]; ``` Your list is *merged* with the casks the rice already installs (Ghostty, AeroSpace, and any roster apps), so you never repeat or clobber them. Don't know the cask name? Search it: ```sh brew search --cask obsidian ``` **Homebrew apps live outside Nix** Casks are installed *by* the rebuild but aren't tracked in Nix generations — so `haus rollback` won't uninstall an app you added, and won't bring back one you removed. Removing a cask from your config also won't delete the app by default (`nebelhaus.homebrew.cleanup` is `"none"`, on purpose, so nothing you installed by hand ever vanishes). To actually remove one: `brew uninstall --zap `. See [Making it yours](/guides/making-it-yours/#homebrew-behaviour) for the `cleanup` / `autoUpdate` / `upgrade` knobs. ## App Store apps Some apps only live in the Mac App Store. The smooth path is **Pounce → Install App → Mac App Store**: type a search, choose the Mac-compatible result, then install it alone or give it a roster key and workspace. Pounce already knows the result's numeric App Store id and bundle id. This path uses [`mas get`](https://github.com/mas-cli/mas) in a visible floating terminal. You need to be signed into the App Store, and a download may ask for Touch ID or your Apple Account. Paid apps must already have been purchased; if the CLI cannot acquire one, Pounce opens the App Store page for you. Unlike Homebrew and Nix choices, App Store acquisition itself isn't captured in a Nix module: it is an Apple Account operation, not part of the Nix generation. A roster entry is still declarative after the app is acquired. ## Already-installed apps For anything already on the machine — Safari, Music, an app you installed by hand — you don't need to install it again. If you want it in the launcher, just add a roster entry with `cask = null`: ```nix nebelhaus.apps.music = { key = "m"; name = "Music"; workspace = "M"; appId = "com.apple.Music"; barIcon = ":apple_music:"; cask = null; }; ``` --- ## A CLI tool you want to keep Command-line tools normally come from **nixpkgs**, which makes them declarative, reproducible, and roll-backable. **Pounce → Install App → Nix packages** searches the exact Nixpkgs revision pinned by your flake and writes a module under `hosts//packages/` that adds the chosen attribute to `environment.systemPackages`. If you prefer to keep the list in Nix source, personal tools go in your host file's home-manager block: ```nix home-manager.users.${username} = { home.packages = with pkgs; [ ripgrep httpie ffmpeg ]; }; ``` Not sure of the exact package name? Search nixpkgs: ```sh nix search nixpkgs ripgrep # …or browse https://search.nixos.org/packages ``` **System vs. home — and where the rice** The rice ships a baseline toolbelt already (`bat`, `fzf`, `lazygit`, `jq`, `gh`, `delta`, `yazi`… — see [The shell](/guides/the-shell/)). Those live in the **nebelhaus repo**, not your host file; only edit them if you're changing the shared distro for everyone. *Your* machine's extras belong in `home.packages` in your host file. (The rare tool that `sudo` or a non-login shell must see can go in `environment.systemPackages` instead — but `home.packages` is almost always what you want.) ## A CLI tool, just to try it hearth wires up [`nix-index`](https://github.com/nix-community/nix-index) and `comma`, so you can run **any** command in nixpkgs without installing it — great for one-offs and "does this even do what I think": ```sh , cowsay "hello fog" # fetches cowsay, runs it, keeps nothing , ncdu # try a disk-usage TUI once ``` If you find yourself reaching for it twice, *then* add it to `home.packages`. ## Tools that need a secret or an env var Some CLIs want an API key. Keep the secret **out of your Nix config** (it'd land in the world-readable Nix store) — read it from a file at shell startup instead: ```nix home-manager.users.${username} = { programs.zsh.initContent = lib.mkAfter '' export OPENAI_API_KEY="$(cat ~/.secrets/openai 2>/dev/null)" ''; }; ``` The `~/.secrets/*` files stay on the machine, ungit-tracked. See [Making it yours](/guides/making-it-yours/#secrets--private-things) for the pattern. --- ## Then apply it 1. `haus rebuild` — builds first, so a typo can't take down your running system. 2. New casks install, new CLI tools land on your `PATH`, and any new roster app gets its letter, workspace, and bar pill — all in one go. 3. If a new roster app doesn't tile on first launch, it's a one-time hiccup; tap , then Backtick to re-sort everything to its workspace. **This is your file — go wild** Your host file is a plain nix-darwin module. Anything nix-darwin or home-manager accepts works here and merges with the rice. Adding apps is just the most common case — see [Making it yours](/guides/making-it-yours/) for the whole cookbook. --- # Window management (prowl) Source: https://nebelhaus.com/guides/window-management/ Tiling windows and the Caps-Lock launcher — how prowl arranges your Mac like a keyboard-driven tiling rig. **prowl** is the tiling room. It wraps [AeroSpace](https://github.com/nikitabobko/AeroSpace) — a tiling window manager for macOS — and adds a **Caps-Lock leader** so you can launch, focus, move, and resize windows without ever reaching for the mouse or a far-flung modifier. Windows arrange themselves into a tree; the keyboard moves them around it. It feels like a tiling Linux rig, but it's native macOS windows underneath. ## The leader key: Caps-Lock prowl remaps **Caps-Lock to F18** and uses it as a leader. There are two ways to use it: - **Tap ⇪** (press and release) → enter **launch mode**: the bar morphs into letter-hint bubbles, and pressing an app's letter launches or focuses it. - Launch mode is also the gateway to **navigate mode** (arrow keys move focus), **resize mode**, workspace jumps, and quick palette actions. Meanwhile, `⌥` (Option/Alt) is the everyday modifier for layouts and workspace-moves. Focus with *arrows* deliberately isn't a bare `⌥` chord: `⌥`, `⌘`, and `⌃` with the arrow keys are all macOS text-navigation, so arrow-focus lives behind the Caps-Lock leader (tap ⇪, then arrow) where nothing collides. No Vim keys required — but `⌥H/J/K/L` is there for the hands that want it. ### Not fond of these keys? Both halves of that vocabulary are options, so none of the above is baked in: ```nix nebelhaus.keys.leader = "caps"; # caps | alt-space | none nebelhaus.keys.windowNav = "alt"; # alt | ctrl-alt | cmd-alt | none nebelhaus.keys.palette = "cmd-space"; # cmd-space | alt-space | ctrl-space | none ``` Three reasons people change them, in rough order of how often: - **Keep Caps-Lock.** `leader = "alt-space"` gives you the launcher without remapping anything. (The remap is re-applied at each rebuild and doesn't survive a reboot, so moving off `"caps"` ends it — at the latest, at next boot.) - **A non-US keyboard layout.** On many layouts `⌥`+letter is how you type accented characters, so a window manager owning `⌥` makes the keyboard unusable. `windowNav = "ctrl-alt"` moves every chord in one go — and the cheatsheet's captions move with them, because both come from the same table. - **Mouse-first.** `leader = "none"` with `windowNav = "none"` leaves the keyboard entirely alone: prowl still tiles, but claims no chords. Reach apps through [Pounce](/guides/pounce/) instead. `palette = "none"` hands ⌘Space back to Spotlight — and only that value takes Spotlight's shortcut away in the first place. Whatever you pick, the in-system cheatsheet (tap the leader, then `/`) and the first-run tour describe the keys you actually have, not these defaults. ## Everyday tiling — main mode | Keys | Action | |---|---| | `⌥/` | Tiles layout (toggles horizontal ↔ vertical split) | | `⌥,` | Accordion layout (toggles horizontal ↔ vertical) | | `⌥F` | Toggle fullscreen | | `⌥Tab` | Jump to the previous workspace (back-and-forth) | | `⌥⇧Tab` | Move the workspace to the next monitor | | `⌥⇧;` | Enter **service mode** | | `⌘Space` | Open [Pounce](/guides/pounce/) | Nothing here names a *workspace* — those are all leader actions. Tap , then a digit (`1`–`4`) or an app's letter to **go there**, or `⇧`+that same key to **throw the focused window there and go with it**. Same motion whether you're heading to a scratch space or to an app, and the two halves of the thought are one `⇧` apart — either way you end up on the workspace you named. To send a window somewhere and stay put, throw it and then `⌥Tab` straight back. ## Launch mode — tap ⇪ Tap Caps-Lock, then: | Key | Action | |---|---| | `` | Launch or focus that app (e.g. `T` = Ghostty, `B` = Zen) | | `1`–`4` | Focus workspace 1–4 | | `⇧1`–`⇧4` | Throw the focused window to workspace 1–4 and follow it there | | `⇧` | Throw the focused window to that app's home workspace and follow it there | | `←↓↑→` | Move focus; enters **navigate mode** (see below) | | `-` / `=` | Enter **resize mode** (shrink / grow) | | `V` | Clipboard history (via Pounce) | | `E` | Emoji picker (via Pounce) | | `Z` | Reopen the last closed app (the ⌘⇧T analog) | | `,` | Open macOS System Settings (mirrors the ⌘, convention) | | `Backtick` | Re-sort every window to its workspace | | `/` | Show the cheatsheet | | `Esc` | Exit launch mode | **Navigate mode** is how you move focus without Vim keys or a text-nav-clobbering chord: tap ⇪, then an arrow moves focus *and* sticks, so further arrows keep moving. Hold `⇧` with an arrow to *move* the focused window instead. `Esc` or `Enter` returns to the desktop. **Resize mode** is a small delight: once you're in it, you can press `-` or `=` repeatedly without re-tapping the leader. `Esc` or `Enter` exits. ## Service mode — ⌥⇧; For the less-frequent structural operations: | Key | Action | |---|---| | `R` | Flatten the workspace tree | | `F` | Toggle floating ↔ tiling for the window | | `Backspace` | Close all windows except the current one | | `⌥⇧H/J/K/L` | Join with the neighbour in that direction | | `↑` / `↓` | Volume up / down | | `⇧↓` | Mute | | `Esc` | Reload config and exit | ## Your app roster The launcher letters, workspace assignments, and the bar's workspace pills all come from **one keyed app map**: `nebelhaus.apps`. Out of the box it's just a terminal (`T`) and a browser (`B`). Entries compose across modules, so add only your own: ```nix nebelhaus.apps.slack = { key = "s"; name = "Slack"; workspace = "S"; appId = "com.tinyspeck.slackmacgap"; barIcon = ":slack:"; label = "Slack"; cask = "slack"; }; nebelhaus.apps.discord = { key = "d"; name = "Discord"; workspace = "D"; appId = "com.hnc.Discord"; barIcon = ":discord:"; cask = "discord"; }; ``` Each field: - **`key`** — the launcher letter (unique across the roster). - **`name`** — the macOS app name (`open -a` uses it). - **`workspace`** — the AeroSpace workspace letter; `null` means launcher-only, no dedicated workspace. - **`appId`** — the bundle id, used to auto-assign the app's windows to its workspace. Find it with `osascript -e 'id of app "Slack"'`. `null` skips auto-assign. - **`barIcon`** — the [SketchyBar app-font](https://github.com/kvndrsslr/sketchybar-app-font) ligature (e.g. `:slack:`) shown in the bar; `null` falls back to the workspace letter. - **`label`** — the cheatsheet caption; `null` uses `name`. - **`cask`** — a Homebrew cask to install it; `null` if it's already installed. - **`enable`** — set `false` to disable a default or imported app by id. - **`order`** — lower numbers appear earlier in the roster. Rebuild with `haus rebuild` and your new letters, workspaces, and bar pills all appear together. **Windows untangle after sleep** macOS loves to scramble window positions when the machine wakes. prowl installs a wake watcher that re-sorts windows to their workspaces automatically — and you can trigger it yourself any time by tapping , then Backtick. ## Turning it off Don't want tiling? Set `nebelhaus.prowl.enable = false;` and the Caps-Lock remap, launcher, and AeroSpace all stay out of your way. The rest of the rice still works. The complete, always-current binding list — including the terminal and Pounce — is on the [Keybindings cheatsheet](/reference/keybindings/). --- # The bar (sill) Source: https://nebelhaus.com/guides/the-bar/ The SketchyBar status bar — workspace pills, weather, media, and how it reflects your tiling in real time. **sill** replaces the macOS menu bar with [SketchyBar](https://github.com/FelixKratz/SketchyBar) — a fully scriptable status bar tuned to the [nebelung](/reference/palette/) palette and wired to your [tiling](/guides/window-management/). When sill is on, the native menu bar is hidden so the bar is the single source of glanceable state. ## What's on the bar **Left — workspace pills.** One pill per workspace in your [app roster](/guides/window-management/#your-app-roster). A pill shows either its workspace letter or the app's bar icon (e.g. `:slack:`), and the focused workspace is highlighted. The front app's title sits alongside. **Right — the status cluster (on by default, each one hideable):** - **Clock** and **battery** - **Wi-Fi** state - **Weather** — a compact pill that opens a full popover on click - **Media** — the currently-playing track, scrolling Any of these five can be switched off individually — see [Toggling bar items](#toggling-bar-items) below. The pills react live: switch workspaces and the highlight follows; tap the Caps-Lock leader and the whole bar flips into launcher letter-mode. ## The weather popover Click the weather pill and a popover blooms open with the full picture: location, condition, current and feels-like temperature, sunrise/sunset, wind, humidity, UV, an hourly strip, and a three-day forecast. It's one of the bar's nicer flourishes and a good demo of what SketchyBar can do. ## Toggling bar items Every right-side pill is a single boolean in `nebelhaus.sill.items`. The five core pills (`clock`, `weather`, `media`, `battery`, `wifi`) default **on**; the extras below default **off**. Set only the ones you want to change: ```nix nebelhaus.sill.items = { weather = false; # drop a default-on core pill cpu = true; # add an off-by-default readout caffeinate = true; # add the keep-awake controller }; ``` A pill set `false` is never created — its update script doesn't even run. **Extra readouts** — harmless anywhere, just off so the default bar stays quiet: - **`cpu`** — total CPU load, as a percentage pill. - **`memory`** — memory-pressure percentage pill. - **`volume`** — output volume / mute state. - **`calendar`** — your next timed event, with a click-popup of the next five. Pulls in `ical-buddy` automatically and reads Calendar, so macOS prompts for Calendar access on first run. - **`caffeinate`** — a coffee pill for preventing idle system sleep. Click it to choose 1, 2, 4, or 8 hours, enter a custom number of hours, keep the Mac awake until stopped, or allow sleep again; right-click stops immediately. While active, the pill shows the time remaining. The assertion is owned by launchd, not SketchyBar, so it survives a bar reload and resumes with the correct remaining time after a rebuild or login. It uses macOS's built-in `caffeinate`—no package or permission prompt. The display can still turn off, and closing a MacBook lid still sleeps it. The same keep-awake engine is available without the pill: ```sh awake 3h awake 90m awake indefinitely awake status awake off ``` **Personal extras** — each targets one person's hardware, service, or workflow: - **`agents`** — a paw pill tracking your agent-worktree panes: amber when one is waiting on you, click for the per-agent list, click a row to jump to its pane. Each row is marked with the client sitting in it (Claude Code, Codex, Opencode), painted in that pane's state colour, and a row clears itself once its pane is gone — however the agent ended. Dormant until a client's hooks fire, so it's harmless if you don't use agents. The same state also shows up **inside** the terminal: a count badge beside the zellij tab name of whichever tab holds agent panes — see [Coding agents](/guides/claude-agents/#which-tab-wants-you). - **`aiUsage`** (was `claudeUsage`, still accepted) — a gauge pill showing how much of your AI usage you've spent: a percentage for subscription limits (Claude Code, Codex), or the day's dollars for token-billed Opencode. It follows whichever provider reported most recently, showing the higher of the 5-hour or weekly window and colouring to match (green under 50%, yellow past 50%, peach past 75%, red at 90%). Click it for every provider's windows with their reset times. Where each row comes from matters, because one of them leaves the machine: - **Claude Code** rides its own statusline feed. Nothing is polled and no token is read, so the row appears once a Claude session first reports and greys out if none has in half an hour. - **Opencode** is read straight out of its local sqlite db. Also no network. - **Codex** is the exception. It keeps no usage data on disk at all — the desktop app holds threads server-side, and only the CLI and the VS Code extension ever wrote rate limits locally — so a disk-fed row silently freezes the day you switch to the GUI app. That row is polled from your ChatGPT account instead (limits are per account, so it counts GUI usage too), authenticated with the OAuth token already in `~/.codex/auth.json`. The refresher renews that token when it nears expiry and rewrites the file in place, keeping a `.bak`. **If you've never logged into Codex on this machine, no call is ever made** — the presence of `auth.json` is the entire opt-in. Claude's row is *pushed* by its statusline; the Codex and Opencode rows are *pulled*, and the pill pulls them itself every few minutes. That matters if you don't use Claude: those two rows used to move only while a Claude statusline was rendering somewhere, so on a Codex or Opencode machine the pill would grey out and stay grey. - **`elgato`** — a pill that toggles an Elgato Key Light on your local network. - **`harvest`** — a [Harvest](https://www.getharvest.com/) time-tracking pill. It reads credentials from a file **you** provide at `~/.config/sketchybar/harvest_secrets.sh`. The **hush** (Do-Not-Disturb) pill is separate: it rides [`nebelhaus.hush.enable`](/guides/hush/), not `sill.items`. **Secrets stay out of Nix** sill never bakes tokens into the store. The harvest plugin sources your `harvest_secrets.sh` at runtime — keep that file out of git. `aiUsage` reads your Codex login from `~/.codex/auth.json` the same way: at runtime, from the file OpenAI's own tooling wrote, never through Nix. ## Turning it off ```nix nebelhaus.sill.enable = false; ``` With sill disabled, the native macOS menu bar is shown again (the rice only hides it when sill is on). Tiling and the rest of the rice are unaffected. ## Under the hood sill is generated entirely from your config. On rebuild it writes `~/.config/sketchybar/` — the main `sketchybarrc`, a `colors.sh` exporting the nebelung palette as `0xAARRGGBB` values, a `workspaces.sh` built from your app roster, and a `plugins/` directory of small shell scripts (one per bar item). The palette and your roster are the single sources of truth, so the bar always matches the rest of the system. --- # The shell (hearth) Source: https://nebelhaus.com/guides/the-shell/ The terminal experience — Ghostty, zellij, zsh, starship, yazi, helix, and a toolbelt of themed CLI tools. **hearth** is the cozy terminal room. It sets up the whole command-line experience — the terminal emulator, the multiplexer, the shell, the prompt, the file manager, the editor, and a toolbelt of modern CLI replacements — every one of them painted in [nebelung](/reference/palette/) colours. ## The stack | Layer | Tool | Notes | |---|---|---| | Terminal | [Ghostty](https://ghostty.org) | font size 19, title bar hidden, auto-attaches to zellij | | Multiplexer | [zellij](https://zellij.dev) | custom tab bar + status bar, grid/spiral/columns layouts | | Shell | zsh | framework-free, fzf-tab, sensible history, declarative Git aliases | | Prompt | [starship](https://starship.rs) | nebelung-tinted | | File manager | [yazi](https://yazi-rs.github.io) | markdown & code previews, image previews via chafa | | Editor | [helix](https://helix-editor.com) | the default `$EDITOR`, nebelung theme | | Jumper | [zoxide](https://github.com/ajeetdsouza/zoxide) | frecency `cd`, `cdi` for an fzf picker | Plus a themed toolbelt: `bat` (aliased over `cat`), `lsd` (over `ls`), `lazygit` (`lg`), `claude` (`c`), `delta` for git diffs, `fzf`, `fd`, `jq`, `gh`, `glow`, `fastfetch`, and `nix-index` + `comma` so you can run any nixpkgs command on demand. ### Git aliases Hearth uses plain zsh—no shell framework—and supplies a finite set of Git shortcuts directly. The names follow the broadly used Oh My Zsh-style vocabulary where conventions agree, while deliberately avoiding ambiguous shortcuts such as `gl`, `gr`, and `gs`, whose meanings differ between popular alias sets. | Area | Aliases | |---|---| | Add | `ga`, `gaa`, `gapa` | | Branch / checkout / clone | `gb`, `gba`, `gbd`, `gbD`, `gbm`, `gco`, `gcb`, `gcl`, `gsw`, `gswc` | | Commit / cherry-pick | `gc`, `gca`, `gcam`, `gcmsg`, `gcn`, `gcp`, `gcpa`, `gcpc` | | Diff / log | `gd`, `gds`, `gdw`, `glo`, `glog`, `gloga` | | Fetch / merge | `gf`, `gfa`, `gfo`, `gm`, `gma`, `gmc`, `gmff` | | Pull / push | `gpl`, `gpr`, `gp`, `gpf`, `gpsup` | | Rebase / repository | `grb`, `grba`, `grbc`, `grbi`, `grbs`, `grt`, `grv` | | Status / stash | `gst`, `gss`, `gsb`, `gsta`, `gstl`, `gstp`, `gsts` | | Tag / worktree | `gt`, `gwt`, `gwta`, `gwtl`, `gwtr` | `g` itself is `git`. Add, replace, or remove shortcuts per host with `nebelhaus.git.shellAliases`: ```nix nebelhaus.git.shellAliases = { gst = "git status --short --branch"; # replace gsync = "git pull --rebase --autostash"; # add gco = null; # remove }; ``` ## Ghostty opens into zellij Launching Ghostty drops you straight into a persistent zellij session called `main`. New panes and tabs inherit your working directory. A few conveniences: - Ghostty's `⌘T`, `⌘P`, `⌘Y`, `⌘C` and friends are **unbound** on purpose — zellij owns those, so the same keys work whether or not you're multiplexed. - `Ctrl-Tab` / `Ctrl-Shift-Tab` walk your tabs in **most-recently-used order**, browser-style — one tap bounces between your two hottest tabs; keep tapping to go further back. (Ghostty forwards the kitty-protocol encoding so zellij sees them.) ### zellij keybindings | Keys | Action | |---|---| | `Super P` | New pane, inheriting cwd — but a shell born inside a `claude --worktree` checkout hops to the repo's **main checkout** | | `Super ⇧P` | New pane, **stay here** — inherits cwd with no hop, so you get a shell right inside the worktree | | `Super T` | New tab at `$HOME` — born named `~` (fresh tabs are never `Tab #N`) | | `Super ⇧T` | New tab at the **focused pane's directory** — with the **same worktree hop** as `Super P` | | `Super F` | **Fullscreen** the focused pane — zoom it to fill the tab, tap again to drop back into the tiled layout | | `Super R` | **Reload the terminal stack** — gracefully quit/reopen Ghostty, then start a fresh zellij server with the same tabs, panes and working directories; live Claude Code panes resume their exact conversations | | `Super Y` | **yazi peek** — a floating browser with live previews; `Enter` on a directory opens a new tab there | | `Super ⇧Y` | **yazi jump** — browse, then drop a shell in the landing directory | | `Ctrl Tab` / `Ctrl ⇧Tab` | Tab history back / forward (most-recently-used) | | `Alt <` / `Alt >` | Cycle swap layouts (grid → spiral → columns) | The reload is also available as `zreload` from a shell. It prepares the recovery layout inside zellij, then hands the quit/delete/reopen sequence to a launchd agent outside the terminal, so killing the old server cannot strand its own script. This is a real Ghostty quit and relaunch, not `reload_config`: that action only reparses Ghostty settings and would leave the old launcher process alive. Each zellij server generation keeps an explicit pane-to-Claude-session manifest, so a foreground tool such as `sourcekit-lsp` cannot be mistaken for the pane that launched it. If any active command pane cannot be proven resumable, reload stops before quitting Ghostty or deleting the session. **Option-click a path** A custom zellij link handler means Option-clicking a file path in the terminal opens a **new tab already `cd`'d there**. Great for jumping into a path a build tool just printed. ### Layouts zellij ships three swap layouts, cycled with `Alt <` / `Alt >`: - **Grid** (default) — the most balanced square arrangement, every pane equal area. - **Spiral** — balanced 50/50 nesting, largest pane top-left. - **Columns** — equal full-height columns side by side. ## yazi previews yazi is set up for fast previewing: markdown renders through `glow`, code through `bat` (syntax-highlighted), and images through `chafa` near-fullscreen inside zellij. Press `Y` to copy a file's *contents* (not just its path), and `Esc` to close a floating peek. In the `Super Y` peek overlay, `Enter` on a **file** pages it fullscreen while `Enter` on a **directory** opens a new zellij tab there (use `l` / `→` to descend into a directory without leaving peek) — so peek doubles as the browse-and-pick "new tab" chooser. ## Choosing your editor helix is the default. `nebelhaus.hearth.editor` is the **one** editor the rice uses everywhere — it's your `$EDITOR` / `$VISUAL`, and it's what every "open in an editor" action launches (the "Nix Config" palette command, the bar's nix-open item, and the file-association hijack below). Those open the target in a new zellij tab running it, so a terminal editor is the natural fit; a GUI editor's CLI works too. Set it in your host file: ```nix nebelhaus.hearth.editor = "nvim"; # any terminal editor # a GUI editor's CLI works too, e.g. "code" or "code -w" to block ``` One related knob: - **`nebelhaus.hearth.hijackFileAssociations`** — off by default; when `true`, makes your editor the default opener for `.json`, `.md`, `.ts`, `.nix`, and similar via [`duti`](https://github.com/moretension/duti). ## Handy shell touches - **zoxide** — `cd` learns your habits; `cdi` opens an fuzzy picker of your frequent directories. - **Git aliases** — a compact, framework-independent set (`g`, `gst`, `gco`, `gcb`, `gp`, `gpf`, `grbi`, `gwt`, …) ships built in — no oh-my-zsh. Add, override, or remove one with [`nebelhaus.git.shellAliases`](/reference/options/#nebelhausgitshellaliases). - **Auto-renaming tabs** — `cd` into a repo and the zellij tab renames itself after it; `cd ~` renames it back to `~`, the name every fresh tab is born with (no `Tab #N` anywhere). - **Claude Code aware** — new shells opened (`Super P` / `Super ⇧T`) inside a `~/.cache/claude-worktrees` checkout hop to the parent repo automatically — and the `Super ⇧T` tab is named after the main repo, not the throwaway worktree. Want a shell *inside* the worktree instead? `Super ⇧P` is the no-hop escape hatch. See [Theming & accents](/guides/theming/) for how every tool above gets its colours, and the [options reference](/reference/options/#nebelhaushearth) for every hearth knob. --- # Coding agents (wt) Source: https://nebelhaus.com/guides/claude-agents/ The rice ships wt — safe, resumable agent worktrees for any git repo and any of Claude Code, Codex or OpenCode, with a live statusline HUD. A productivity feature, not a nebelhaus-only tool. If you drive [Claude Code](https://claude.com/claude-code) from a nebelhaus machine, the rice hands you a small tool called **`wt`** — it makes running several agents at once safe, and makes closing a pane something you never have to think about. It works on **any git repo**, not just nebelhaus, and you get it for free the moment the rice is on your `PATH`. **Where wt sits among the CLIs** `wt` is one of the [rice's four CLIs](/start/the-family/#the-clis-at-a-glance). It's the one that isn't *about* nebelhaus at all — it's a general Claude Code productivity tool the rice happens to ship, the same way it ships `haus` for your machine. You don't need to be a nebelhaus contributor to use it. ## The problem it solves Run two Claude agents in the same checkout and they fight: one switches the branch out from under the other, uncommitted edits collide, and closing a pane mid-thought can lose work. `wt` fixes both by giving **every agent its own checkout**. Press **`Super c` (⌘C)** in any repo's terminal tab and the rice spawns a fresh Claude Code session in an **isolated git worktree**: - its own checkout on a `worktree-` branch, branched from the repo's local `HEAD`; - living **outside** the repo, under `~/.cache/claude-worktrees//`, so your working tree stays clean and no build tool ever trips over it; - so any number of agents can hack in parallel without ever touching each other's branch — or yours. **`Super ⇧c` (⌘⇧C)** spawns exactly the same isolated agent, but **in place of the focused pane** instead of splitting a new one beside it — for when the tab is already as subdivided as you want it and the pane you're looking at has done its job. The pane it replaces is only *suspended*, never killed: quit the agent and your shell comes back, focused, scrollback intact. `Ctrl Alt Shift c` is the resident variant: the one agent per tab that's allowed to edit the checkout you're actually looking at. Prefer the launcher? **Spawn Agent** in Pounce (`⌘Space`, type "spawn") does the same thing from the GUI, for [whichever client you've made the default](#spawn-from-pounce--and-choose-the-default-client): pick a repo, type what you want done, and it creates a worktree **named after the task** — not the random `luminous-twirling-codd` an unattended agent would coin — and drops a session into that repo's tab. Naming the branch from the prompt is the whole point; the palette is just the shortest path to it. **Closing a pane never loses work** When you close a `Super c` pane, the rice doesn't just delete the checkout. Any uncommitted edits are first parked as a `wip:` commit on the branch, then the checkout is removed. **Only branches whose work has already merged get reaped** — everything unmerged survives on its branch, and the agent transcript survives too. The checkout dir is disposable; your work isn't. ## Resuming a session Because the work lives on the branch (and the chat in your client’s transcript), a closed pane is never a dead end. Run `wt` bare to see everything you can pick back up, across **every** repo: ```sh wt # list every parked / live agent worktree, all repos ``` ``` 🌫 agent worktrees you can resume (wt , or /) repo name state agent last commit nebelung sparkle parked claude 2 hours ago — wip: auto-saved on pane close pounce quick-emoji live codex 10 min ago — add fuzzy weights ``` To pick one back up, name it — `wt` rebuilds the checkout and reopens the chat in the same directory, so that worktree's client resumes with its full history: ```sh wt sparkle # rebuild the checkout + reopen the chat (that worktree's client) wt nebelung/sparkle # qualify with the repo when a name exists in two repos ``` ## Spawn from Pounce — and choose the default client For a named worktree without first finding the right terminal tab, summon Pounce and run **Spawn Agent**. Pick a repository, describe the task, and it creates a `worktree-` checkout before opening the configured client in that repo's zellij tab. Set the client declaratively: ```nix nebelhaus.agents.default = "codex"; # or "claude" (default), or "opencode" ``` `wt` records that client with the worktree. Changing the option therefore only changes **new** spawns: a parked Codex task still reopens through `codex resume`, and an OpenCode task through `opencode --continue`. The option drives the ⌘C keybinds and the `c` shell alias too. Only Claude Code can make its own worktree (its native `--worktree` flag, which fires `wt`'s create hook); for Codex and OpenCode, ⌘C runs **`wt new`** instead — the same checkout, branch, registry entry and parent session, made from the outside. A separate option says which clients are **installed**: ```nix nebelhaus.agents.clients = [ "claude" "opencode" ]; # the default ``` `agents.default` must be one of them, and the rebuild says so by name if it isn't — which is the point: naming a client you didn't have used to fail at spawn time, *inside the pane*, after the worktree already existed. Pounce's Spawn Agent still checks `command -v` before it creates anything, for the case the assertion can't cover (a hand-managed install that moved). **Overlay, don** Want a patched build of a client — a wrapper, a postInstall patch? **Overlay the package** (`claude-code`, `codex`, `opencode`) rather than putting your own derivation in `home.packages` beside it. Two derivations shipping the same `bin/claude` collide in one profile, and the rebuild fails with `two given paths contain a conflicting subpath`. There is also **Spawn Agent with Screenshot**. It uses macOS's normal area selector (`screencapture -i`) before the repository/prompt flow, then keeps the PNG under `~/.cache/nebelhaus-agent-screenshots/` for the agent. Codex receives it as an initial image; Claude and OpenCode receive its file path in the first prompt so they can inspect it. Keeping it as a separate palette entry matters: a task whose text happens to contain “screenshot” can never steal Return in the free-text prompt field. ## Cross-repo work: `wt child` Sometimes an agent working in one repo needs to change **another** one — say a session in the workshop that has to touch a sub-repo. Don't reach for a raw `git worktree add`: that skips `wt`'s registry, so the statusline never learns to track it and its PR goes invisible. Use `wt child` instead: ```sh workshop_root="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")" cd "$(wt child "$workshop_root/nebelung")" ``` That creates a worktree of the *other* repo, **registers it as a child of your current pane** (so its PR shows up in the statusline where you're working), and prints only the new checkout path — hence the `cd "$(…)"`. Resolving the workshop root through Git keeps the command independent of where you cloned it. ## Cleaning up Most cleanup is automatic: closing a pane reaps a merged branch, and every `wt` listing first self-heals by sweeping parked branches whose PR has since merged. But some things end a pane *without* the graceful hook — a reboot, a crash, or a `wt child` checkout (which the hook never owns, since the pane that made it isn't the one it belongs to). For those, sweep on demand: ```sh wt reap # reap every LANDED worktree now — parked ones, plus # clean & merged live checkouts no pane is open in ``` `wt reap` is deliberately conservative: it leaves anything **dirty**, anything **unmerged**, and any checkout a **pane is still open in** — not only yours but any other session's, because a just-merged branch looks exactly like a finished one while its agent is still sitting in it. Only landed work that nobody is standing in gets removed. ## Setting work aside: `wt park`, never `git stash` `git stash` looks like it belongs to the checkout you're standing in. It doesn't. The stash stack lives in the repo's shared `.git` dir, so **every agent worktree of a repo — and the main checkout — push and pop the same stack.** Two panes stashing at once means either can pop the other's entry into a tree that never asked for it, which is a confusing mess at best and lost edits at worst. So park instead of stashing: ```sh wt park "half-done refactor" # whole dirty tree → one wip: commit on THIS branch wt unpark # rewind it — changes back, uncommitted ``` `wt park` commits everything — tracked edits, untracked files, deletions — as a single `wip:` commit on the branch only this pane has checked out. It's the same thing the pane-close hook does automatically, just on demand. Nothing is shared, so no other agent can touch it; it survives a pane close; and `wt` lists it as that worktree's last commit. `wt unpark` puts the changes back in the working tree, uncommitted, exactly as they were. **unpark won** If the `wip:` commit has already been pushed — it's visible in an open PR — `wt unpark` refuses, because rewinding it locally turns a file restore into a force-push. It prints the manual escape hatch (`git reset --mixed HEAD^`) and leaves the commit alone. Parking on a branch that isn't a `worktree-*` one (say, `main`) works, but warns: a `wip:` commit shouldn't drift onto a branch you push. ## Which tab wants you Run agents in eight tabs and the question is always the same one: which of them is sitting on a permission prompt? The rice answers it in the tab bar itself — a filled badge beside the name of any tab holding agent panes. The badge carries a count — how many agents live in that tab — on a background coloured by what the most urgent one is doing: | Badge | Means | |---|---| | **peach** | an agent is blocked on you — a permission prompt, or it's asking a question | | **sky** | an agent is mid-turn, working | | **green** | the agent finished its turn; nothing is waiting | | *(no badge)* | no agent panes in that tab | The number is the exact count of agent panes in the tab; the colour is the most urgent state among them, so peach always wins: the tab that needs you never hides behind a tab that's merely busy. And it's *status*, not a notification — a green badge stays green until that agent starts another turn or its pane goes away. Looking at it doesn't dismiss it. This is the same signal as the [`agents` bar pill](/guides/the-bar/), reported by the client's own lifecycle hooks (which is why it's exact rather than guessed — the agent says what it's doing, nothing scrapes the screen for it). The pill is for when you're out in the GUI; the badge is for when you're already in the terminal. It lights up as soon as an agent pane reports, and stays invisible if you never run one. Every client reports through one command, `agent-state `, which the rice puts on your `PATH`: | Client | Wiring | Who writes it | |---|---|---| | **OpenCode** | `~/.config/opencode/plugin/nebelhaus-agent-state.js` | the rice, for you | | **Codex** | `UserPromptSubmit` / `PermissionRequest` / `Stop` in `~/.codex/hooks.json` | the rice, for you — Codex then asks you to **review and trust** those hooks the first time it starts, which the rice deliberately doesn't bypass | | **Claude Code** | `UserPromptSubmit` / `Notification` / `Stop` / `SessionEnd` hooks in `~/.claude/settings.json` | you — Claude owns that file and rewrites it, so the rice never touches your hooks | A pane's row also disappears on its own once the pane is gone, whether or not its client said goodbye. That matters most for Codex, whose hooks have no session-*end* event at all — but it's the same thing that clears the row of a Claude agent you killed, or one that crashed. ## The statusline HUD The rice points Claude Code's status bar at a small HUD driven off `wt`'s registry, so you can see where every agent stands without leaving the pane. **Row one is this session's own worktree** — its name, a status token, and its GitHub PR pill. The **rows below are the worktrees this session spawned** (across whatever repos they live in), each with its repo, name, token, and PR state. The cross-repo GitHub lookups run detached and cached, so the bar never stalls your prompt. The token is one mutually-exclusive slot: | Token | Means | |---|---| | `⏏` | branch is merged into `main` — closing the pane reaps the worktree | | `N^` | `N` commits on the branch, not merged yet | | `+A -D` | uncommitted line changes, when nothing's committed yet | | *(nothing)* | identical to `main` — no news is good news | ## Links — every URL a pane's session saw The same statusline pass that drives the HUD also notes which Claude transcript each pane is running, and Pounce turns that into a command. Press **⌘L** in the terminal (or summon the palette with **⌘Space** and run **Links**) to get every URL from the focused pane's whole Claude Code session — newest first, including links buried in collapsed tool output or that scrolled away hours ago. **⏎** opens the pick in your browser. GitHub PR and issue links come back enriched with their real title and open/closed/merged state — a parallel, cached `gh` fetch capped at ~2s, so a slow network degrades to plain links rather than a hung palette. A pane not running Claude Code falls back to its visible scrollback. ## Command summary | Command | What it does | |---|---| | `wt` | List every parked/live agent worktree across all repos (self-heals first) | | `wt ` | Resume one: rebuild its checkout and reopen its recorded agent chat | | `wt resume ` | The same thing, spelled out | | `wt reap` | Sweep every landed worktree now (keeps dirty/unmerged/any open pane) | | `wt child ` | Make a worktree of another repo as a child of this pane (cross-repo) | | `wt park [label]` | Set the dirty tree aside as a `wip:` commit on this branch (use instead of `git stash`) | | `wt unpark` | Undo the last `wip:` commit — changes back in the working tree, uncommitted | | `wt new [name]` | Make a worktree of **this** repo and open the default client in it — what `Super c` runs when that client isn't Claude Code | | `wt create` / `wt remove` | The hooks Claude Code's `--worktree` fires — you don't call these by hand | | `wt agent default` | Print the configured spawn client | **Related shell touches** New panes born inside a worktree hop back to the repo's **main checkout** by default (`Super P`), so a shell lands somewhere stable; `Super ⇧P` is the no-hop escape hatch that keeps you *inside* the worktree. See [The shell](/guides/the-shell/#zellij-keybindings) for the full set. Hacking on nebelhaus itself? The [Contributing](/internals/contributing/) page shows how agent worktrees plug into the family's `bench try` / PR / ship flow. --- # Touch ID for sudo (collar) Source: https://nebelhaus.com/guides/touch-id/ Fingerprint authentication for sudo — even inside tmux or zellij — via collar. **collar** is the small-but-mighty auth room. It enables **Touch ID for `sudo`** so privileged commands take a fingerprint instead of a typed password — and, crucially, it makes that work **inside a terminal multiplexer** like zellij or tmux, where stock Touch-ID-for-sudo silently fails. ## What it turns on collar configures two PAM settings for `sudo`, and installs one sudoers rule: ```nix security.pam.services.sudo_local.touchIdAuth = true; # fingerprint for sudo security.pam.services.sudo_local.reattach = true; # make it work in tmux/zellij ``` - **`touchIdAuth`** adds the Touch ID PAM module to `sudo`. - **`reattach`** pulls in [`pam_reattach`](https://github.com/fabianishere/pam_reattach), which reattaches the sudo process to your GUI login session. - **`/etc/sudoers.d/darwin-rebuild`** exempts one command from authentication entirely — see below. ## The one command that never asks Activating a build is the privileged thing this rice does constantly, and a prompt per rebuild buys nothing: you already authenticated to *build* it. So collar grants that one command a pass: ``` ALL=(ALL) NOPASSWD: /run/current-system/sw/bin/darwin-rebuild ``` `haus rebuild` and `bench try switch` both route through that exact path, so rebuilds run straight through. Nothing else is exempt. **Why the stable path, not the store path** macOS 26.6 shipped sudo 1.9.17, which stopped dereferencing a command's symlink before matching sudoers. A rule written against `/nix/store/…-darwin-rebuild/…` now matches only if you type that store path literally — which nobody does — so the exemption silently stops applying and Touch ID quietly returns on every rebuild. Matching `/run/current-system/sw/bin/darwin-rebuild` is what survives. One cosmetic wrinkle: nix-darwin's `environment.etc` is symlink-only, so the rule lands as a `0444` store symlink and `visudo -c` may grumble that it "should be 0440". Harmless — sudo refuses group/world-**writable** sudoers files, not merely readable ones. ## Why the reattach matters Without `pam_reattach`, running `sudo` **inside** zellij or tmux detaches from the Aqua GUI session, so the Touch ID prompt has nowhere to appear — it just beachballs. With it, the prompt reattaches to your GUI session and the sensor lights up as expected. If you cancel, it falls back to a typed password. **Yes, it works inside zellij** The payoff: type `sudo -v` in a zellij pane and the macOS Touch ID dialog floats up right over the multiplexer. Touch the sensor, and the command runs — no password typed. (`darwin-rebuild` is the exception, and a happier one: it doesn't even ask.) ## What it does *not* do collar deliberately stays out of key management. **YubiKey and GPG** are kept out of Nix: key material, smartcard setup, and `pinentry` live outside the rice, handled by `gpg-agent` + `pinentry-mac` at runtime. This keeps secrets off the Nix store and lets you manage hardware keys the normal way. Commit signing itself is wired up in [hearth](/guides/the-shell/) from your `nebelhaus.git.signingKey` — collar only handles the `sudo` authentication path. ## Nothing to configure collar has no options of its own — it's on as part of the rice. If you ever want to opt out, remove or override the two PAM settings (and the sudoers rule, if the passwordless rebuild isn't to your taste) in your host file. For most people it just works from the first `switch`, and typing your password to `sudo` becomes a memory. --- # Focus & DND (hush) Source: https://nebelhaus.com/guides/hush/ One quiet switch — bar pill, palette command, and CLI — for macOS Do Not Disturb, your Slack status, and your own hooks. **hush** is the quiet room. One switch — the bell pill in the bar, "Toggle Hush" in the palette, or `hush` in a terminal — that turns macOS **Do Not Disturb** on or off, optionally sets your **Slack status** (and silences Slack on your phone), and runs any **hooks** you add. All three surfaces call the same engine, so they can never disagree. Because hush drives the *real* macOS Focus, everything Focus already does comes along free: if **Share Across Devices** is on, your iPhone and iPad hush too, and the "allowed to break through" list is whatever you've curated in System Settings. ## How the toggle works (and why it's unusual) Apple ships no public API or CLI to set a Focus — every "focus CLI" on GitHub is secretly a Shortcuts wrapper. nebelhaus takes a different path: 1. **The binding is nix config.** macOS has a hidden keyboard shortcut slot — *"Turn Do Not Disturb On/Off"*, symbolic hotkey **175**, disabled by default and buried in Settings. Every `switch` binds it declaratively to **⌃⌥⇧⌘ F13**, a chord nothing else uses. 2. **The engine presses it.** `hush` synthesizes that keypress. No Shortcuts app, nothing to author by hand, and the binding heals itself on every rebuild. State is read from the Focus database (`~/Library/DoNotDisturb/DB`) when the calling app has Full Disk Access, so the pill stays truthful even when you toggle Focus from Control Center or your phone; without that grant, hush falls back to remembering its own last toggle. ## The one-time grants Synthesizing a keypress needs **Accessibility**, and exact state reads need **Full Disk Access** — both granted once, per app, in System Settings → Privacy & Security. Run `hush doctor` any time: it checks each piece and prints the exact fix for whatever is missing. - **Palette toggles work out of the box** — palette commands run under the pounce daemon, which already holds an Accessibility grant (that's what [`pounce.signingIdentity`](/reference/options/#nebelhauspouncesigningidentity) keeps stable across rebuilds). - **For the bar pill**, add **sketchybar** to Accessibility. - **For the CLI**, add your terminal (Ghostty) — many setups already have it. - **Optional but recommended:** add the signed **Pounce.app** (or your terminal + sketchybar) to **Full Disk Access** so `hush status` reads the real Focus state instead of guessing from its own memory. **Why grants stick** macOS keys TCC grants to a code-signing identity. The rice re-signs Pounce with your stable identity precisely so these checkboxes survive rebuilds — tick them once, forget them. ## The Slack leg macOS Focus already silences Slack banners *on your Mac*. The Slack leg does what Focus can't: tells your **teammates** (status + 🔕) and silences your **phone** (Slack's own DND, all devices). While hushed, your previous status is stashed and restored on unhush. It's off by default because it needs a personal Slack token: 1. Create a Slack app for yourself at [api.slack.com/apps](https://api.slack.com/apps) ("From scratch", any name, your workspace). 2. Under **OAuth & Permissions → User Token Scopes**, add `users.profile:write` and `dnd:write`, then **Install to Workspace** and copy the **User OAuth Token** (`xoxp-…`). 3. Stash it in your Keychain — no secret ever touches your config or the Nix store: ```bash security add-generic-password -s hush-slack -a "$USER" -w 'xoxp-…' ``` 4. Point hush at it in your host file: ```nix nebelhaus.hush.slack = { enable = true; tokenCommand = "security find-generic-password -s hush-slack -w"; statusText = "heads down"; # the defaults, shown for flavor statusEmoji = ":no_bell:"; snooze = true; # silence Slack push on all devices }; ``` `hush doctor` verifies the token against Slack's API once it's wired. ## Hooks — make hush yours Every hush/unhush also runs your own scripts, each called with `on` or `off`. Failures are logged, never fatal — a broken hook can't wedge the toggle. ```nix nebelhaus.hush.hooks = [ ./onair-light.sh # turn the Elgato red while hushed "/Users/ada/bin/pause-music" # strings run as-is, so $HOME paths work ]; ``` Ideas: pause the music, flip an on-air light, start a Harvest timer, shove chatty apps to a parking workspace with `aerospace`. ## Honest scope - hush flips the built-in **Do Not Disturb**, not named Focus modes — those have no symbolic hotkey to bind. - It doesn't manage **which apps or people break through**; that's your Focus allowlist, curated once in System Settings → Focus. - Without Full Disk Access, `hush status` is hush's own memory — it can drift if you toggle Focus elsewhere. The bar pill self-corrects on the next real toggle either way. ## Troubleshooting **Run `hush doctor` first.** It checks the hotkey binding, the state-read grant, and the Slack token, and prints the fix for each. - **Click the pill, nothing happens** — sketchybar has no Accessibility grant yet; you'll get a notification saying so. Grant it, or toggle from the palette (which needs nothing). - **Pill shows the wrong state after toggling from Control Center** — grant Full Disk Access for exact reads; otherwise the pill catches up the next time hush itself toggles. - **The chord stopped working** — `darwin-rebuild switch` re-binds hotkey 175 on every activation; a rebuild heals it. --- # Pounce — the launcher Source: https://nebelhaus.com/guides/pounce/ A native, keyboard-first command palette for macOS. Summon, aim, pounce — and every command is just a file. **Pounce** is a free, open-source, native command palette for macOS — a fast, dmenu-style picker you summon with a hotkey. Hit **⌘Space**, fuzzy-type, hit **Return**. It launches apps, runs commands, answers quick questions inline, and hosts a handful of built-in modes (clipboard, emoji, screenshots, and more). Its whole personality is one idea: **every command is a file.** Drop a shell script in a folder and it's in the palette — no plugin API, no extension store, no account. It's [MIT licensed](https://github.com/nebelhaus/pounce/blob/main/LICENSE) and free — every feature on this page, no paid tier. **Standalone or part of the rice** Pounce is a full member of the [family](/start/the-family/) but installs on its own from Homebrew — no Nix required. Inside nebelhaus, the `pounce` module wires it up as a login daemon and re-signs it so its permissions survive rebuilds. ## Summon it - **⌘Space** — the default hotkey. Registered in-process by the daemon, so it opens instantly. (nebelhaus disables Spotlight's ⌘Space so Pounce can own it.) - Change it in [config](/reference/pounce/#config-file): the `hotkey` object takes a `key` and `modifiers`. **If ⌘Space does nothing on a stock Mac** Installing standalone, Spotlight still owns ⌘Space — and it wins silently. Registration *succeeds*, so pounce can't tell; macOS simply routes the key elsewhere. The daemon checks for this at startup and says so in its log, naming the shortcut that beat it. Free the key in **System Settings → Keyboard → Keyboard Shortcuts → Spotlight**, then restart the daemon. (Rice users get this for free — nebelhaus turns Spotlight's binding off.) ## What you can do **Launch apps** Type a few letters — `saf` → Safari. Freshly-installed apps get boosted so they're easy to find right after install. **Run commands** Your scripts and the built-ins appear alongside apps, ranked by how often you use them. **Clipboard history** Browse and restore recent copies in a two-pane view; optionally auto-paste into the app you came from. **Find Files** Search files and folders by name as you type, over Spotlight's index. ⏎ opens, ⌘⏎ reveals in Finder, ⌥⏎ copies the path. **Quick answers** Math, unit, time-zone, and currency queries answer inline as a pinned first row — `⏎` copies the result. **Emoji, screenshots, more** Built-in modes for an emoji grid, a screenshot browser, a camera peek, and a searchable cheatsheet. ## Navigating the palette | Key | Action | |---|---| | Type | Fuzzy-search in real time | | `↑` / `↓` | Move the selection | | `Return` | Run the default action | | `⇧Return` | Insert a newline — the query field is multi-line | | `⌘Return` | The modifier action (often "Reveal in Finder" for apps) | | `⌥Return` / `⌃Return` | Alternate actions, when shown | | `Tab` | Cycle sections (or emoji categories) | | `Esc` | Dismiss | The action bar at the bottom always shows which modifiers do what for the selected row, so you don't have to memorize them. ## How ranking works Results are ordered by a blend of three signals, so the thing you want is usually already on top: 1. **Fuzzy match quality** — consecutive runs, matches at word boundaries, and how much of the name your query covers all score higher. Titles count double subtitles. 2. **Frecency** — frequency × recency, with a 72-hour half-life. The apps and commands you actually use float up. 3. **Freshly-installed boost** — apps installed in the last week get a temporary bump (halving every two days) so new installs are easy to find. ## Quick answers Some queries are answered **in the palette** instead of being fuzzy-matched against apps and commands. The answer pins to the top as its own row, and `Return` copies it: | Type | Get | |---|---| | `2*847`, `2^10`, `sqrt(9)`, `round(2pi)` | Arithmetic, with the interpretation shown beside it | | `100 + 10%`, `20% of 150` | Percentages | | `72 f in c`, `1 pound in kg`, `10 km in mi`, `1 gib in mib` | Unit conversion | | `100 usd in eur`, `$100 in eur`, `50 euros in yen` | Currency conversion | | `14:00 utc in pst`, `2pm utc in tokyo`, `9pm pst in utc` | Time-zone conversion | There's no trigger prefix — a query that no engine can parse just falls through to the normal results, and anything without a digit skips the engines entirely (so `safari` and `1password` stay searches). Output follows your locale, so `1/4` copies `0,25` on a German Mac. Currency uses the European Central Bank's daily reference rates (fetched from `api.frankfurter.app`, refreshed at most every 12 hours, cached to `~/.local/share/pounce/currency-rates.json` so it keeps answering offline). It's the only network request Pounce makes; set `quickAnswers.currency` to `false` in [config](/reference/pounce/#config-file) to switch it off entirely. Nothing about your usage is ever sent anywhere. ## A window switcher, not an app switcher (opt-in) Pounce ships an **MRU window switcher**: tap the chord to bounce to your last window, hold it and keep tapping to walk older ones, or type while holding to fuzzy-filter. It moves by *window*, not by app, so two Ghostty windows are two rows. It's **off by default** because it needs the Accessibility grant to install an event tap. Turn it on with the `windows` block in [config](/reference/pounce/#config-file) (or [`nebelhaus.pounce.windowSwitcher`](/reference/options/#nebelhauspouncewindowswitcher) in the rice, where it defaults on). If you run AeroSpace, rows carry the window's workspace and focusing goes through `aerospace focus --window-id` so a window parked elsewhere surfaces properly; without it, everything degrades to plain focus. ## Two-step commands (submenus) Some commands open a second picker instead of acting immediately — "Brew Services" lists services, "Force Quit" lists processes. Pounce swaps the list **in place** with no window flicker. Any command you write can do this by declaring `# pounce: submenu = true`. See [Writing pounce commands](/guides/pounce-commands/). ## Built-in commands Out of the box you get: **Activity Monitor**, **Brew Services**, **Camera**, **Capitalize**, **Clipboard History**, **Emoji**, **Find Files**, **Force Quit**, **Lock Screen**, **Lowercase**, **Ports**, **Report Issue**, **Screenshots**, **System Settings**, and (Homebrew installs) **Update Pounce**. Each is just a script you can read, copy, or replace. **Capitalize** and **Lowercase** rewrite the text you have selected in *any* app — they copy the selection, run it through a one-line shell filter, and paste it back via `pounce --transform`. Swap `tr` for anything and you have your own text action. **Update Pounce** self-updates in place, and what it does follows how you installed Pounce: a Homebrew build runs `brew upgrade pounce` and restarts the service; a copy dragged to `/Applications` downloads the new release and swaps itself out; a Nix or rice install is pointed at the flake (`haus update`) rather than overwritten, since the next rebuild would revert it. It runs in the background and reports via notifications — see the [Pounce reference](/reference/pounce/) for the full per-install breakdown. Every System Settings pane is its own command too, so typing "Displays" or "Accessibility" jumps straight there instead of dropping you at the front door. **Report Issue** opens a pre-filled GitHub issue with your pounce and macOS versions already in the body — the rice ships a matching one for nebelhaus itself. ## Optional plugins Beyond the built-ins there's a shelf of optional plugins, **off by default** because each one assumes a specific tool, service, or app on your machine: | Plugin | What it does | Needs | |---|---|---| | **Audio Devices** | Switch sound output & input from the palette | `brew install switchaudio-osx` | | **Bluetooth** | Connect & disconnect paired devices (AirPods, keyboards, …) | `brew install blueutil` | | **Caffeinate** | Keep the Mac awake — indefinitely or on a 30m/1h/2h timer | nothing (system `caffeinate`) | | **Docker** | Start, stop, restart & tail logs of containers; offers to start the engine | any engine that answers `docker ps` (Docker Desktop, OrbStack, colima) | | **GitHub** | Jump to your PRs, review requests, issues & repos in the browser | `gh` CLI, authenticated | | **Spotify** | Playback controls: play/pause, next, shuffle, now-playing | Spotify.app (drives it over AppleScript) | | **SSH Hosts** | Pick a host from `~/.ssh/config` (Includes too); ⏎ connects, ⌘⏎ copies the command | hosts in `~/.ssh/config` | | **Tailscale** | Connect/disconnect, copy your tailnet IP or any peer's | `tailscale` CLI (ships inside Tailscale.app) | On Nix, enable them by id when consuming the `pounce-commands` package: ```nix pounce-commands.override { plugins = [ "docker" "ssh" "tailscale" ]; } ``` An enabled plugin behaves exactly like a built-in — same palette discovery, same `pounce-` bin for hotkey bindings, still shadowable from your user dir. Not on Nix? Every plugin is still just one script: copy it from [`pkgs/pounce-commands/optional/`](https://github.com/nebelhaus/pounce/tree/main/pkgs/pounce-commands/optional) into `~/.config/pounce/commands/` and it's live on the next ⌘Space. **Bluetooth needs a one-time grant** The first time the Bluetooth plugin runs it raises the system permission dialog for Pounce (macOS gates device access behind TCC). Approve it once and the device list appears; the plugin walks you there if the dialog was dismissed. ## Install it standalone ```sh brew tap nebelhaus/tap brew install pounce brew services start pounce # start the daemon pounce --request-accessibility # one-time, for paste-back features ``` The formula installs a prebuilt `Pounce.app` signed with our Apple **Developer ID** and **notarized** by Apple — it opens straight away, no Gatekeeper prompt and no compile step. Requires **macOS 14 Sonoma or later on Apple Silicon**; the release is arm64 only. Because that signing identity is stable, the Accessibility grant **survives `brew upgrade`** — you approve it once, not once per release. Or with Nix: `nix run github:nebelhaus/pounce -- --help`. ## Grant Accessibility (for paste-back) Clipboard auto-paste and emoji paste synthesize a ⌘V into your previous app, which macOS gates behind Accessibility: ```sh pounce --request-accessibility # approve the dialog pounce --check-accessibility # prints true when granted ``` **Nix rebuilds and Accessibility** Nix builds are ad-hoc signed, so a plain rebuild would drop the Accessibility grant. Inside nebelhaus, set `nebelhaus.pounce.signingIdentity` and the rice re-signs a stable copy of the app with your identity, so the grant persists. See [Pounce config](/reference/pounce/). Full configuration, CLI flags, and file paths are on the [Pounce config & CLI](/reference/pounce/) reference. To extend it, head to [Writing pounce commands](/guides/pounce-commands/). --- # Writing pounce commands Source: https://nebelhaus.com/guides/pounce-commands/ Every command is a file. How to add your own commands to Pounce with a shell script and an optional metadata header. Pounce has no plugin SDK. A command is **one shell script** with an optional metadata header. Drop it in a folder and it's in the palette on the next open — no restart, no manifest, no account. ## Your first command 1. Make the user commands directory if it doesn't exist: ```sh mkdir -p ~/.config/pounce/commands ``` 2. Create `~/.config/pounce/commands/say-hello.sh`: ```bash #!/bin/bash # pounce: name = Say Hello # pounce: description = A friendly notification # pounce: icon = hand.wave osascript -e 'display notification "🐾" with title "Pounce"' ``` 3. Open Pounce (**⌘Space**), type `say`, and hit **Return**. Your command is right there — the notification fires. That's the whole model. No build step; the registry is re-scanned every time you summon the palette. ## The metadata header The `# pounce: key = value` lines at the top describe how the command appears. All are optional; parsing stops at the first non-header line (within the first 30 lines), and whitespace around the `=` is tolerant. | Key | Meaning | Default | |---|---|---| | `name` | Title shown in the palette | the filename (without `.sh`) | | `description` | Subtitle | *(empty)* | | `icon` | An [SF Symbol](https://developer.apple.com/sf-symbols/) name (e.g. `wifi`, `sparkles`) | `sparkles` | | `submenu` | `true` / `1` — the command re-invokes Pounce for a second step | `false` | ## Two-step commands (submenus) Set `submenu = true` and pipe your options through `pounce` again to build a two-step flow — the list swaps in place with no flicker: ```bash #!/bin/bash # pounce: name = Brew Services # pounce: description = Start or stop a service # pounce: icon = cup.and.saucer # pounce: submenu = true service=$(list_services | pounce -p "Service:") [ -n "$service" ] && toggle_service "$service" ``` When the second step is a **search** rather than a list — the user types free text and your script answers by fetching — pass `--chain`. On an empty match Enter hands the raw text back, and `--chain` tells Pounce that text feeds another `pounce`, so the window holds its loading skeleton instead of fading out between steps: ```bash query=$(printf '' | pounce --chain -p "App Store — type a search, then Enter") [ -n "$query" ] && mas search "$query" | pounce -p "Install:" ``` Anything you can pipe into `pounce` becomes a picker. Which brings us to… ## Pounce as a generic picker Beyond commands, `pounce` is a dmenu-style picker: pipe it lines, it returns the chosen one on stdout. ```sh echo -e "one\ntwo\nthree" | pounce -p "Pick:" ``` A piped list keeps **the order you gave it** — only the launcher's own apps get sorted, so a ranking your script already computed (an App Store relevance list, say) survives into the picker instead of being alphabetized away. Each input line can carry extra columns, tab-separated, for richer rows: ``` title subtitle icon actions group ``` - **actions** — `label | key:label | key:label…` (the first is Return, the rest are modifier combos). - **group** — an optional section header to bucket rows under (as Force Quit does with *Applications* / *Background*). ## Where commands are discovered Pounce merges commands from several directories. Later sources **shadow** earlier ones by filename, so your own always win: 1. `$POUNCE_BUILTIN_DIR` — the built-ins that ship with the package. 2. `$POUNCE_EXTRA_COMMAND_DIRS` (colon-separated) — layers added by Nix consumers like the rice. 3. `$POUNCE_COMMAND_PATH` (colon-separated) — ad-hoc extra directories. 4. `~/.config/pounce/commands` — **yours**, highest precedence. **Override a built-in** Want to change what "Lock Screen" does? Drop your own `lock.sh` in `~/.config/pounce/commands/` — same filename, and yours takes over. **Data payloads & support files** Pounce lists every `.sh` file in a command directory as a command. A non-`.sh` file — a `.tsv` or `.json` data catalog — is ignored wherever it sits, so it never turns into a runnable row. The case that still needs nesting is a *helper* that is itself a `.sh` but shouldn't be a command: put it in a subdirectory (e.g. `data/`), which Pounce doesn't descend into. ## Tips - Scripts don't need the executable bit — Pounce runs them with `bash` either way — but they **must** end in `.sh`: that suffix is how Pounce finds them, and a file without it is never listed as a command. - Keep long-running work fast to start; the palette dismisses as soon as the script is spawned. - For grouped or dynamic output, remember the tab-separated column format above. - Read the [built-in commands](https://github.com/nebelhaus/pounce/tree/main/pkgs/pounce-commands/commands) for real, copy-pasteable examples of submenus, grouping, and icons. See the [Pounce config & CLI reference](/reference/pounce/) for every flag and config key. --- # Messages (trill) Source: https://nebelhaus.com/guides/trill/ A native, keyboard-first iMessage/SMS/RCS client that reads chat.db read-only — install, the one-time permissions, the feature tour, and the privacy posture. **trill** is your Messages, native. A fast, flat, keyboard-first client for iMessage, SMS, and RCS in a real macOS window — it reads your real conversations straight from Apple's `chat.db` (**always read-only**) and sends by driving Messages.app. It's themed in [nebelung](/reference/palette/) like the rest of the house, ships by default in the rice, and also stands alone on any Mac. **Read-only by design** trill's own code never writes to `chat.db` — every connection is opened read-only, and sends go through Messages.app, which owns its own storage. That's the whole trust story: it reads everything, changes nothing. ## Install trill rides in the rice by default (`nebelhaus.trill.enable = true` — see [options](/reference/options/#nebelhaustrillenable)). To install it standalone, or on a Mac without the rice: ```sh brew install --cask nebelhaus/tap/trill ``` The app is signed with our Apple **Developer ID** and **notarized** by Apple, so the cask installs it and it opens straight away — no Gatekeeper prompt. If you build or copy the app by hand instead of via the cask, macOS may quarantine your copy; clear it yourself: ```sh xattr -dr com.apple.quarantine /Applications/Trill.app ``` ## First run — the permissions trill opens in **Synthetic Fixture** mode (deterministic sample data, no permissions). Switch the provider to **Messages** in Settings to read your real conversations. It asks for only what a given feature needs, when it needs it: 1. **Full Disk Access** — required to read `~/Library/Messages/chat.db`. Grant it to the **Trill app bundle** (not Terminal) in System Settings → Privacy & Security → Full Disk Access. Selecting the Messages provider runs a read-only probe first and links you here if the grant is missing. macOS only hands the new access to a fresh process, so trill offers a **Relaunch Trill** button rather than making you quit and reopen it yourself. 2. **Automation ("control Messages")** — prompted the first time you send. This is how trill sends without ever writing to `chat.db`. 3. **Contacts** *(optional)* — grants names **and** contact photos. Without it, names still resolve by reading the local AddressBook (covered by Full Disk Access); only photos are missing. 4. **Notifications** *(optional)* — prompted on first launch with the live provider, for incoming-message banners with inline reply. Grant something while trill is in the background and it re-checks on return, so Automation and Contacts start working the moment you switch back — no Recheck button, no restart. Full Disk Access is the one exception, hence the relaunch. **Marking read is local-only** Marking a conversation read *upstream* would require writing to `chat.db`, which trill never does. Opening a thread clears its badge **locally** and the mark persists in trill's own database — your phone's badge is unaffected. ## What you can do trill is a triage tool, not just a viewer — everything Messages can't do: - **Read everything** — iMessage/SMS/RCS threads, reactions grouped by emoji, quoted replies with jump-to-original, edited/unsent markers, inline image thumbnails, Quick Look on attachments, group avatars, and a Dock unread badge. - **Send** — text and attachments (paperclip / drag-drop / paste), ⌘N to compose to any contact with autocomplete, and a 5-second **undo-send** window. - **Search** — ⌘K command palette to jump anywhere, ⇧⌘F global message search with operators (`from:`, `in:group`, `has:link`, `has:image`, `before:`/ `after:`, `is:unread`), and ⌘F find-in-thread (⌘G / ⇧⌘G to step matches). - **Universal Library (⌘⇧L)** — one browser for every image, link, file, and saved message across *all* conversations, with jump-to-source. - **Organize** — folders/tags, VIP (always-notify + always-pin), snooze, archive, mute, and service filters (toggle iMessage/SMS/RCS), all composable and local-only. - **Triage** — ⇧⌘R filters to threads whose last message is *from them* and still unanswered; a per-thread **stats** panel shows counts, reply-time medians, and most-active hours. - **Compose faster** — snippets and slash-commands (`/shrug`, `/date`, …) from one `/`-trigger picker, fill-in-the-blank templates, and persistent drafts (with a **Drafts** filter to find the ones you abandoned). - **Scroll back without asking** — threads load 100 messages at a time and fetch the next page automatically as you near the top. **Load Earlier Messages** is still there if you'd rather drive. - **Stay ambient** — a menu-bar mini-inbox with unread count, conversation tabs (⌘T) to keep several threads open, and ⌘[ / ⌘] to walk recently-viewed threads. ### Keyboard reference trill ships an in-app cheat-sheet — press ⌘/ any time. The essentials: | Keys | Action | |---|---| | ⌘K | Command palette (jump to any thread/action) | | ⇧⌘F | Global message search | | ⌘F / ⌘G | Find in thread / next match | | ⌘⇧L | Universal Library | | ⌘N | Compose to a contact | | ⌘T / ⌘⇧] ⌘⇧[ | New tab / switch tabs | | ⌘[ / ⌘] | Back / forward through recent threads | | ⇧⌘R | Needs-reply filter | | ⌘J | Jump to date | | ⌘+ / ⌘− / ⌘0 | Zoom | ## Theme it trill ships all four [nebelung](/reference/palette/) variants compiled in — dark or **latte**, normal or **high contrast** — and picks between them in **Settings ▸ Theme**: `Follow macOS`, `Dark`, or `Light`, plus which palette each polarity uses. Nothing to rebuild, and it applies the moment you pick it. The accent, display density, and zoom live in the same pane. Anything else is a file. Drop a flat `name → "#hex"` JSON — nebelung's own `*.hex.json`, or a stock Catppuccin flavor — into `~/.config/trill/themes/` and it appears in those menus: ```sh mkdir -p ~/.config/trill/themes curl -o ~/.config/trill/themes/my-palette.json https://example.com/palette.json ``` A file **shadows a built-in of the same name**, so naming yours `nebelung.json` replaces trill's idea of nebelung. That's how the rice keeps an installed trill current: on nebelhaus it writes every rendered variant there and names the pair your [`theme.flavor` / `theme.contrast`](/guides/theming/) select, so trill follows the house palette. Your own pick in Settings still wins. ## Export a conversation (or all of them) trill exports threads to **Markdown**, plain text, or HTML — a single thread and date-range from the conversation header (`⌘`-up-arrow button), or **every** conversation as one Markdown file per thread in a `.zip` from Settings → Export. The bulk export is framed as *"ready to hand to an LLM"*: it's the clean corpus for feeding your own history to a model. It's read-only throughout — nothing is sent anywhere or written back to `chat.db`. ## Your writing style, as a document **Messages → Writing Style Profile…** builds a portrait of how you actually write — across everyone, or for one thread from the conversation toolbar. Copy it or save it as Markdown. It's computed on your Mac by counting things, not by asking a model: no AI, no network, nothing leaves the machine. The output is meant for you to paste into an assistant yourself, so it can write in your voice without ever being handed your messages. ## Honest scope - trill **displays** tapbacks, threaded replies, and edits, but can't *send* them — the native (AppleScript) send path has no automation surface for them, so those actions aren't offered rather than faked. - Attachments your Mac has offloaded to iCloud show as **"In iCloud"** rather than downloading on demand — fetching them is Messages.app's job, and trill says so instead of pretending the file is missing. - It reads `chat.db`; it does not sync, relay, or push. There's no server and no network messaging (a future [BlueBubbles relay](https://github.com/nebelhaus/trill/blob/main/ARCHITECTURE.md) is designed-for but not built). - Any future AI/summarization feature is gated on a separate privacy review — local-first, nothing leaves the device without an explicit decision. ## Troubleshooting - **"Messages" provider won't turn on** — it needs Full Disk Access on the **Trill app bundle** (not Terminal). Grant it in System Settings, then reselect the provider. - **First send does nothing / prompts forever** — approve the **Automation** ("control Messages") prompt; that's the send path. - **Names show as numbers** — grant **Contacts** (or rely on AddressBook via Full Disk Access); photos need Contacts specifically. - **Gatekeeper blocks a hand-copied build** — `xattr -dr com.apple.quarantine /Applications/Trill.app` (the cask does this for you). - **Full guide to the app's design and roadmap** lives in the [trill repo](https://github.com/nebelhaus/trill) (`README`, `ARCHITECTURE.md`, `docs/`). --- # Theming & accents (nebelung) Source: https://nebelhaus.com/guides/theming/ One silver-mist palette across every app — swapping the accent, raising the contrast, and switching to light mode. The whole system shares one palette: **[nebelung](/reference/palette/)**, a silver-mist Catppuccin variant. It's Catppuccin with the blue pulled out of the greys and the accents calmed down — a fog-grey scheme for people who find most dark themes too loud. Grey is the point. nebelung renders that one palette onto **50+ tools** — Ghostty, zellij, starship, bat, delta, lazygit, fzf, yazi, helix, btop, Slack, the Zen browser, and more — so the terminal, editor, git, and browser all speak the same colours. The rice wires up the ones it installs for you; the rest are one roster entry away (see [Apps you added yourself](#apps-you-added-yourself)). Three knobs, in the order most people reach for them: | Option | What it changes | |---|---| | [`theme.accent`](#swapping-the-accent) | the one hue that tints highlights | | [`theme.contrast`](#contrast) | how far text separates from its background | | [`theme.flavor`](#light-mode) | dark or light | They compose, and all three take effect on the next `haus rebuild`. ## Swapping the accent The neutral greys stay fixed; only the **accent** hue changes. Set it once in your host file: ```nix nebelhaus.theme.accent = "sapphire"; ``` Then `haus rebuild`. The accent re-tints the tools that render per-accent variants — **lazygit** borders, **fzf** highlights, **yazi**, and the **Zen browser** — while the single-file dotfiles (Ghostty, starship, tmux, bat, zellij) keep their built-in theme. ### Available accents The default is `mauve`. Any of the 14 Catppuccin accent names works: `rosewater` · `flamingo` · `pink` · `mauve` · `red` · `maroon` · `peach` · `yellow` · `green` · `teal` · `sky` · `sapphire` · `blue` · `lavender` See their exact hex values on the [palette reference](/reference/palette/). **Pounce, Trill and Perch follow too** The [Pounce](/guides/pounce/) launcher, the [Trill](/guides/trill/) Messages client and the Perch notch shelf compile the nebelung variants in, so their UI matches the rest of the rice out of the box — and each loads any other palette at runtime, from `~/.config/pounce/themes/`, `~/.config/trill/themes/` and `~/.config/perch/themes/`. The rice installs every nebelung variant in each and names the one `theme.flavor` / `theme.contrast` select, so they follow those options like every other tool — no rebuild involved. Trill also exposes the choice in its own **Settings ▸ Theme**, which wins over what the rice writes; Perch has no picker of its own — the rice's word is final, and it always tracks the macOS light/dark appearance. ## Contrast The default greys are deliberately soft. If text doesn't separate enough from its background — a bright room, a glossy screen, or just eyes that prefer more edge: ```nix nebelhaus.theme.contrast = "high"; ``` Same hues, same accents; the neutral ramp is pulled apart in OKLCH so every step gains separation. Body text goes from **11.3:1** to **19.9:1** against the background, which is measured in nebelung's CI rather than eyeballed — and having now been looked at on a real screen, it reads crisp rather than harsh. **What it reaches, and what it doesn** This recolours everything the rice colours — Ghostty, bat, delta, lsd, yazi, zellij, glow, starship, lazygit, the bar, Zen, Obsidian. It does **not** touch native macOS apps, which have their own switch: ```nix nebelhaus.accessibility.increaseContrast = true; ``` The two are complementary, and a genuinely high-contrast Mac wants both. That one needs Full Disk Access on whatever you rebuild from — see its [option reference](/reference/options/#nebelhausaccessibilityincreasecontrast). ## Light mode ```nix nebelhaus.theme.flavor = "latte"; ``` Light mode is **not** the dark palette inverted. nebelung is "Catppuccin with the blue stripped out", and that recipe says nothing about dark — so it's applied to Catppuccin **Latte** instead of Mocha to get the same theme in the other polarity: the same warm-grey neutral ramp, the same calmed accents, read the other way up. Because it's a real re-render rather than a recolour, tools take their light-mode branches properly — terminal ANSI black/white swap around, the Zen browser switches its `prefers-color-scheme` block, delta marks itself `light = true`. Light mode lands at **7.0:1** for body text on its own, so it's legible before you reach for contrast. The two axes compose: | | `contrast = "normal"` | `contrast = "high"` | |---|---|---| | `flavor = "mocha"` | 11.3:1 | 19.9:1 | | `flavor = "latte"` | 7.0:1 | 9.9:1 | **macOS** The rice leaves **System Settings ▸ Appearance** alone, in both directions, so a light rice on a dark Mac looks half-finished. Set macOS's own Light/Dark by hand to match. (This isn't laziness: turning dark mode *on* is one declarative setting, but turning it *off* means deleting a default rather than writing one, which nix-darwin has no way to express.) **Pounce** and **Trill** are the exception in the other direction: both read their palette at runtime and follow macOS's Light/Dark themselves, so on a dark Mac they stay dark even under `flavor = "latte"`. Pin them to the flavor with `nebelhaus.pounce.followSystemAppearance = false` / `nebelhaus.trill.followSystemAppearance = false`. The three hand-made [wallpapers](/reference/options/#nebelhausthemewallpaper) do keep their dark palette — their pixels are already coloured. `wallpaper = "bold"` is generated and follows your accent. ## Obsidian vaults Obsidian keeps themes and appearance settings **inside each vault**, so the rice doesn't guess which folders are yours. List the vaults you want Nebelung to manage as home-relative paths: ```nix nebelhaus.hearth.obsidianVaults = [ "Library/Mobile Documents/iCloud~md~obsidian/Documents/notes" ]; ``` Then `haus rebuild`. For every listed vault, Hearth: - copies the generated full Nebelung theme into `.obsidian/themes/Nebelung/`; - selects Nebelung's appearance (dark today, regardless of `theme.flavor` — Obsidian is one of the places light mode does not yet reach); and - disables the obsolete `nebelung` CSS snippet if it was enabled. The rest of `appearance.json` is merged and preserved. A path is skipped with a warning unless it already contains `.obsidian`, so a typo won't create an empty lookalike vault. List multiple paths to keep several vaults in sync; the default `[]` touches none. ## Apps you added yourself Everything above covers the tools the rice installs — it owns their configs and themes each one properly. Apps **you** put in your [roster](/guides/adding-apps/#a-gui-app-with-a-launcher-key-the-roster) get the same treatment automatically, as long as the name you keyed the entry under matches a nebelung port: ```nix nebelhaus.apps = { zed = { key = "e"; name = "Zed"; appId = "dev.zed.Zed"; cask = "zed"; }; warp = { key = "w"; name = "Warp"; appId = "dev.warp.Warp-Stable"; cask = "warp"; }; }; ``` On the next `haus rebuild`, each one's Nebelung theme is written where that app looks for themes — `~/.config/zed/themes/`, `~/.warp/themes/` — in your current flavor, contrast and accent, and re-written whenever you change them. Nothing else to wire. The key is the **roster id**, not the display name: `zed`, not `zed-editor`. Set `nebelhaus.theme.ports.enable = false` to turn the whole pass off. ### Which ones need a click Dropping a theme file is enough for apps that read a fixed path. Plenty of others — Xcode, Warp, OBS, JetBrains, Tabby — have no file interface for *choosing* a theme; the file is put where they look, and the one-time pick stays yours. A handful more (Slack, Raycast, qBittorrent) can't be installed by file at all. Rather than leave you to discover that months later wondering why an app still looks stock, `haus doctor` tells you exactly where each one stands: ``` 🌫 Nebelung theme ✓ gitui — themed (~/.config/gitui/theme.ron) ⓘ Xcode — placed at ~/Library/…/Catppuccin Mocha.xccolortheme — pick it in the app's settings ⓘ Slack — not installed — copy the comma-separated hex string → Slack ▸ Preferences ▸ Themes ▸ paste ``` A ✓ is live now. A ⓘ tells you the one move left. nebelung publishes which is which per tool — the full table is in its [ports documentation](https://github.com/nebelhaus/nebelung/blob/main/docs/ports.md). **Merges are reported, never applied** Ports installed by merging into an existing config file (VS Code's `settings.json`), or needing a compile step first (qBittorrent), are listed with instructions but never written. Silently half-editing a config you own is worse than telling you what to do. ## Why bake, not load at runtime? Colours are generated at **build time** from a single source, not loaded from loose theme files. That means the palette can never drift out of sync: change nebelung, `nix flake update`, rebuild, and every tool gets the exact same new values at once. No "I updated the terminal but forgot lazygit" surprises. ## Using nebelung outside the rice nebelung is a standalone colour system — you can theme any tool without the rest of nebelhaus. Two ways: - **Copy a rendered theme.** Each tool's theme is generated into the flake's output tree; grab the file for your tool and point its config at it. See the [palette reference](/reference/palette/#using-it-in-your-own-tools) for the per-tool paths. - **Consume the flake.** Add the input and reference the built output: ```nix inputs.nebelung.url = "github:nebelhaus/nebelung"; # then: ${nebelung.packages.${system}.default}//... ``` The flake also exposes a raw `palette` attrset (`name → "#hex"`) for configs you generate yourself. ## Tuning the palette itself Want a different fog? The palette is computed in [OKLCH](https://oklch.com/) space from a handful of knobs in nebelung's `scripts/generate-palette.mjs` — the grey's hue and chroma, how much the accents are calmed, which Catppuccin flavor to strip, and how far to push the contrast — then rebuilt with `./build.sh`. That's a nebelung-repo change rather than a host-file one; the [palette reference](/reference/palette/#tuning-the-palette) covers the details. --- # Keeping in sync (haus) Source: https://nebelhaus.com/guides/staying-in-sync/ Daily driving — updating, rebuilding, rolling back, and keeping your machine current with the haus CLI. Once you're set up, day-to-day life with nebelhaus runs through one command: **`haus`**. It wraps the Nix commands you'd otherwise type by hand — build, switch, update, roll back — with safe defaults. **haus vs. bench vs. wt** The `haus` on your `PATH` (documented here) manages **your machine** — it's all you need as an end user. The rice also ships **[`wt`](/guides/claude-agents/)** for Claude Code agent worktrees (useful to anyone, contributor or not), and family *contributors* get a separate **[`bench`](/internals/contributing/)** CLI in the workshop for moving changes between repos. See [the CLIs at a glance](/start/the-family/#the-clis-at-a-glance) for the full map. ## The everyday loop Edit your config, then apply it: ```sh haus edit # open your host file in $EDITOR # …make changes… haus rebuild # build first, then switch — a failed build never activates ``` `haus rebuild` always builds before switching, so a config with an error can't take down your running system — you just see the build fail and fix it. ## Staying current The rice lives upstream. To pull new versions: ```sh haus update # bump the nebelhaus pin, then rebuild ``` This updates the `flake.lock` in `~/.config/nix` to the latest nebelhaus and rebuilds in one step. Because inputs are pinned, nothing changes until you run this — your machine stays exactly as it was until you choose to move it. ## When something goes wrong ```sh haus rollback # atomically return to the previous generation haus generations # list generations you can roll back to haus doctor # health check: Nix, Xcode CLT, GUI agents, cask drift ``` Every `switch` creates a new generation; `rollback` steps back to the last one instantly. For macOS *settings* (not packages), the APFS snapshot the installer took is your coarser rewind. ## The full command set | Command | What it does | |---|---| | `haus rebuild` | Build, then switch — your everyday apply | | `haus update` | Update the nebelhaus pin, then rebuild | | `haus rollback [N]` | Return to the previous generation (or generation `N`) | | `haus generations` | List available generations | | `haus status` | Current generation and how stale the rice is | | `haus edit` | Open your host file in `$EDITOR` | | `haus doctor` | Diagnose Nix / CLT / agent health and cask drift | | `haus btm` | On macOS Tahoe+, diagnose Background Task Management blocks on nix login items | **From the palette, too** [Pounce](/guides/pounce/) ships a **Rebuild** command — ⌘Space, type "rebuild" — that runs `haus rebuild` in a floating terminal, plus a "Nix Config" command that opens `~/.config/nix` in your editor. Handy when your hands are already on the launcher. ## Catching up on another machine If you rebuild from a second Mac (or after shipping changes from elsewhere), just pull your config repo and rebuild: ```sh cd ~/.config/nix && git pull haus rebuild ``` Since everything is in git and pinned, the second machine converges to the exact same system. For the deeper picture of why pins work this way, see [How the flakes fit together](/internals/flakes/). --- # Moving to a new Mac Source: https://nebelhaus.com/guides/new-mac/ Your whole setup is text in git — restore it in a few commands, plus the short list of things Nix deliberately leaves outside itself. This is the payoff for describing your machine as text: a new Mac (or a wiped one) is a **restore**, not a weekend of clicking through System Settings. Your config is in git; you point a fresh machine at it and it converges to the exact same system. A few things live *outside* Nix on purpose — secrets, keychain trust, hardware keys. Those are the manual bits, and they're a short, honest checklist. ## The happy path (you already have a nebelhaus config) If your `~/.config/nix` is pushed to git somewhere, restoring is two moves: 1. **Point the installer at your config.** The same one-liner that scaffolds a fresh setup also *restores* an existing one — it installs the Xcode CLT and Determinate Nix if they're missing, then clones your repo into place and skips the interview entirely: ```sh curl -fsSL https://nebelhaus.com/init.sh | bash -s -- --from ``` 2. **Build, then switch:** ```sh cd ~/.config/nix nix build .#darwinConfigurations.$(scutil --get LocalHostName).system \ && sudo ./result/sw/bin/darwin-rebuild switch --flake . ``` (Prefer doing it by hand? `xcode-select --install`, install [Determinate Nix](https://docs.determinate.systems/), and `git clone` your config to `~/.config/nix` — that's all `--from` does.) That first build downloads the world and takes a while; after it, `haus` is on your `PATH` and the machine is yours again. **New hostname? Add a host file.** If the new Mac's hostname differs, either reuse an existing host file by passing `--flake .#`, or copy `hosts//` to `hosts//` and wire it into `flake.nix`. The identity and app choices come along for free. ## What comes back on its own Once that switch finishes, the rebuild has already restored: - **Every declared GUI app** — anything with a `cask` in your roster or in `homebrew.casks` reinstalls itself. - **Your whole shell** — Ghostty, zellij, zsh, starship, yazi, helix, and the CLI toolbelt, themed and configured. - **All your CLI tools** from `home.packages`. - **The macOS defaults** the rice manages (Dock, Finder, trackpad, key repeat, the Caps-Lock remap, ⌘Space). - **The background agents** — tiling, the bar, and the launcher, launched via launchd so they survive the next cold boot. ## The manual bits Nix doesn't carry By design, a handful of things never enter your Nix config. Walk this list once on a fresh machine: 1. **Secrets.** Copy your `~/.secrets/*` files over securely (AirDrop, a password manager, an encrypted drive). They're git-ignored on purpose, so they don't ride along in the clone. 2. **Commit signing keys.** If you sign commits, import your GPG/SSH key material and set up `gpg-agent` + `pinentry-mac` (or your YubiKey). The key *id* is in your config; the key *material* is not. 3. **Pounce's Accessibility grant.** macOS ties this to the machine, so re-grant it once: ```sh pounce --request-accessibility ``` If you use `nebelhaus.pounce.signingIdentity`, make sure that signing certificate is in the new machine's keychain (import it, or create a fresh Apple Development cert and update the SHA-1 in your host file) so the grant survives future rebuilds. See [Pounce config](/reference/pounce/). 4. **Apps outside the config.** Anything you installed by hand won't reappear — reinstall those, and consider moving the ones you care about into your config (a [cask or `masApps`](/guides/adding-apps/#app-store-apps)) so *next* time they do. 5. **App logins & licences.** Nix restores the app; it can't restore your session. Sign back into the handful of apps that need it. **Do future-you a favour** Every app you move from "installed by hand" into `homebrew.casks` (or the roster) is one less thing to remember next migration. The goal is a machine you could wipe on a Tuesday without flinching. ## Coming from a hand-configured Mac Not restoring — adopting nebelhaus on a Mac you've already set up by hand? The [install](/start/install/) flow is built for exactly that, and it's careful: - **Your Homebrew casks are kept.** The preflight audit lists what it sees; **nothing is removed**. - **Dotfiles are backed up,** not clobbered — anything the rice manages gets a `.backup` copy first. - **Your current macOS settings can be captured** into your new host file, so the switch doesn't fight preferences you already like. - **The interview** collects your name, email, accent, editor, and which rooms you want — then scaffolds `~/.config/nix` and commits it. Nothing activates until you run the build command it prints. Then it's the normal [first run](/start/first-run/), and from there everything on [Making it yours](/guides/making-it-yours/) applies. **Your two undo cards** Adopting the rice takes an APFS/Time-Machine snapshot first (a coarse rewind for macOS *settings*), and every `switch` is a Nix generation you can `haus rollback` to. Homebrew apps are never auto-deleted. You're not walking a tightrope without a net. ## Where to go next - [Making it yours](/guides/making-it-yours/) — now that it's back, tune it. - [Keeping in sync](/guides/staying-in-sync/) — keep this machine and the next one converged. - [How the flakes fit together](/internals/flakes/) — why pinning makes any of this reproducible. - [Leaving nebelhaus](/guides/leaving/) — the reverse trip, when you want to remove a room or take the whole house down cleanly. --- # Leaving nebelhaus Source: https://nebelhaus.com/guides/leaving/ Remove one room, roll back a generation, or take down the whole house — cleanly, in the smallest move that solves the problem. nebelhaus is opinionated, not possessive. You can remove one room, return to an earlier generation, uninstall the macOS system layer, or remove Nix entirely. These are different operations of very different sizes. Start with the smallest one that solves the problem. **Before changing anything, keep your config** `~/.config/nix` is your machine-specific configuration, not disposable generated state. Archive it before you remove anything: ```sh cp -R ~/.config/nix ~/.config/nix.nebelhaus-archive ``` A git remote is better if the config contains no secrets. Either way, inspect it before sharing or backing it up somewhere public — [Making it yours](/guides/making-it-yours/#secrets--private-things) covers keeping secrets out of the repo, and the gitleaks hook that catches the ones you miss. ## Choose your exit | What you want | Use this path | |---|---| | Undo the most recent rebuild | [Roll back one generation](#roll-back-one-generation) | | Stop using tiling, the bar, Pounce, or Hush | [Remove one room](#remove-one-room) | | Remove the nix-darwin-managed system but keep Nix | [Take down the system layer](#take-down-the-system-layer) | | Remove Nix itself | [Remove Determinate Nix](#remove-determinate-nix) | | Remove standalone Homebrew Pounce | [Remove standalone Pounce](#remove-standalone-pounce) | ## Roll back one generation Every successful switch creates a generation. Return to the previous one with: ```sh haus rollback ``` List the available generations or choose a specific one: ```sh haus generations haus rollback ``` Rollback restores Nix-managed packages, agents, shell configuration, and paths from that generation. See [the haus CLI](/reference/haus/) for the full command set. It does **not** reliably restore every macOS preference changed by a prior activation. Dock, keyboard, Finder, menu-bar, wallpaper, Spotlight, file-association, and similar settings may need to be changed back in System Settings, or declared with the values you want in your config. ## Remove one room The visible rooms can be disabled independently in your host file: ```sh haus edit ``` ```nix { nebelhaus.prowl.enable = false; nebelhaus.sill.enable = false; nebelhaus.pounce.enable = false; nebelhaus.hush.enable = false; } ``` Apply the change: ```sh haus rebuild ``` This keeps the base nebelhaus system and your Nix configuration. It only disables the selected rooms: - `prowl` — AeroSpace tiling, Caps-Lock launcher, and window-management bindings - `sill` — SketchyBar and the custom top bar - `pounce` — the Pounce daemon, palette hotkey, and Accessibility-dependent features - `hush` — the Hush CLI, palette command, and status item After rebuilding, verify the macOS controls those rooms used — especially Caps Lock, the native menu bar, Spotlight's shortcut, and Accessibility permissions — are set the way you want. See [nebelhaus.\* options](/reference/options/) for what each toggle governs. ## Take down the system layer This removes nix-darwin's system integration while leaving the Nix installation available for other projects. Run nix-darwin's current uninstaller: ```sh sudo nix --extra-experimental-features "nix-command flakes" \ run nix-darwin#darwin-uninstaller ``` If that cannot run, try the locally installed copy: ```sh sudo darwin-uninstaller ``` Read the plan before confirming. The uninstaller is expected to remove nix-darwin-managed system links, static files, and launchd services, then restore the underlying Nix daemon supplied by the installer. Restart the Mac after it completes. A restart is not a personality flaw. ### Keep or archive your config The uninstaller does not make `~/.config/nix` useless. Keep it if you might return, move it out of the active path, or delete it only after inspecting it. A reversible archive: ```sh mv ~/.config/nix ~/.config/nix.nebelhaus-archive ``` ## Restore replaced dotfiles nebelhaus configures Home Manager to preserve a conflicting file with the suffix `.backup` before replacing it. Common examples include: ```text ~/.zshrc.backup ~/.zshenv.backup ~/.config/starship.toml.backup ~/.config/git/config.backup ``` There may also be backups inside managed configuration directories. Find them before restoring anything: ```sh find "$HOME" -maxdepth 5 -name '*.backup' -print ``` Inspect each backup, confirm the Nix-managed target is gone, then move it back manually. For example: ```sh ls -l ~/.zshrc ~/.zshrc.backup mv ~/.zshrc.backup ~/.zshrc ``` Do not overwrite a new file blindly. The backup belongs to the version that existed at the first conflict, which may not be the version you want today. ## Homebrew applications By default, nebelhaus uses Homebrew cleanup mode `none`. It does not remove apps simply because they are absent from the current configuration. Review the packages declared in your archived host file and compare them with what Homebrew currently owns: ```sh brew list --formula brew list --cask ``` Remove only the applications you no longer want: ```sh brew uninstall brew uninstall --cask ``` Use `--zap` only when you also intend to remove the application's related data: ```sh brew uninstall --cask --zap ``` nebelhaus deliberately does not guess which applications are still yours. That would be an exciting policy in all the wrong ways. ## Remove standalone Pounce This section is for Pounce installed directly through Homebrew, not Pounce managed as a nebelhaus room. (Removing the room is just `nebelhaus.pounce.enable = false;` above.) Stop its service, uninstall it, and optionally remove the tap if you use nothing else from it: ```sh brew services stop pounce brew uninstall pounce brew untap nebelhaus/tap ``` Your personal commands and configuration live under `~/.config/pounce`. Keep them, archive them, or remove them separately after inspection: ```sh mv ~/.config/pounce ~/.config/pounce.archive ``` Then open **System Settings → Privacy & Security → Accessibility** and remove or disable Pounce if it remains listed. ## Remove Determinate Nix Only do this after removing nix-darwin. Removing Nix affects **every** Nix-managed project and tool on the Mac, not only nebelhaus. The Determinate installer provides its own uninstaller: ```sh sudo /nix/nix-installer uninstall ``` Read the uninstall plan before confirming. When it finishes, restart the Mac. If the command fails, stop and use Determinate's current macOS uninstall troubleshooting guide rather than manually deleting random pieces of `/nix`. ## macOS settings to verify Nix generations and uninstallers cannot promise to reconstruct every preference that existed before installation. Check these after leaving: - Dock autohide and orientation - key repeat and press-and-hold behaviour - Finder extensions, hidden files, path bar, status bar, and view style - native menu-bar visibility - Caps Lock behaviour - Spotlight's `⌘Space` shortcut - wallpaper - file associations, if `nebelhaus.hearth.hijackFileAssociations` was enabled - Pounce Accessibility access - Touch ID for `sudo` The bootstrap attempts to create a local APFS snapshot before installation. Treat restoring that snapshot as a coarse recovery operation: it may also revert unrelated changes made after the snapshot. Prefer targeted restoration unless the machine genuinely needs the wider rewind. ## Verify the house is down After restarting, check the pieces relevant to the path you chose: ```sh command -v haus || echo "haus is no longer on PATH" command -v darwin-rebuild || echo "darwin-rebuild is no longer on PATH" brew services list | grep -i pounce || true launchctl list | grep -Ei 'nebelhaus|pounce|aerospace|sketchybar' || true ``` A matching AeroSpace or SketchyBar process is not automatically residue; you may have installed it independently. The question is whether nebelhaus still manages it. If you removed Nix entirely: ```sh command -v nix || echo "nix is no longer on PATH" test ! -e /nix && echo "/nix is gone" ``` ## Report an incomplete departure A departure bug is a launch blocker. Open an issue on [github.com/nebelhaus](https://github.com/nebelhaus) with: - Mac model and macOS version - whether Nix existed before nebelhaus - the removal path you followed - the exact command and output - what remained afterward - relevant screenshots or logs, with secrets and identity information removed Do not include tokens, private keys, secret values, or unredacted personal files in an issue. --- # nebelhaus.* options Source: https://nebelhaus.com/reference/options/ Every option you can set in your host file — types, defaults, and what each one changes. These are the `nebelhaus.*` options you set in your host file at `~/.config/nix/hosts//default.nix`. Everything here is optional unless noted; the defaults are a complete, working system. Apply changes with `haus rebuild`. Each option lists its **type** and **default** under its name, and links to the file that declares it. ## nebelhaus.git Your commit identity — set your own. It stays in [your host file](/internals/flakes/#your-config-is-a-thin-consumer). ### `nebelhaus.git.email` `string` · default `""` Git user.email for commits. Example: ```nix "ada@example.com" ``` Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ### `nebelhaus.git.name` `string` · default `""` Git user.name for commits (hearth wires it into home-manager). Example: ```nix "Ada Lovelace" ``` Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ### `nebelhaus.git.shellAliases` `attribute set of (null or string)` · default `{ }` Per-host additions and overrides for Hearth's built-in Git shell aliases. Values are shell command strings; null removes a built-in. Hearth deliberately owns a compact, framework-independent default set, so this changes only Git shortcuts and does not require a shell plugin manager. Example: ```nix { gst = "git status --short --branch"; # replace a built-in gsync = "git pull --rebase --autostash"; # add one gco = null; # remove one } ``` Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ### `nebelhaus.git.signingKey` `string` · default `""` GPG key id for signing commits/tags. Empty disables commit signing. Key material + any YubiKey/smartcard setup live outside Nix (gpg-agent + pinentry-mac). Example: ```nix "6F7BD6F43A7C1420" ``` Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ## nebelhaus.apps The shared app roster: one entry per app, driving the launcher key, its workspace, the bar pill, the cheatsheet, and optionally its Homebrew cask. ### `nebelhaus.apps` `attribute set of (submodule)` · default `{ }` The shared app roster, keyed by a stable app id. This is the canonical, composable source for AeroSpace launcher keys and workspaces, SketchyBar pills, the pounce cheatsheet, and optional Homebrew casks. Attribute-set entries merge across Nix modules, so a host, an imported file, and pounce's "Install App" command can each contribute one app without parsing or replacing a monolithic list. Set an entry's enable field to false to remove it, or override individual fields by app id. Example: ```nix { slack = { key = "s"; name = "Slack"; workspace = "S"; appId = "com.tinyspeck.slackmacgap"; barIcon = ":slack:"; cask = "slack"; }; } ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..appId` `null or string` · default `null` Bundle id, used for the AeroSpace `on-window-detected` auto-assign rule and the wake-time re-sort. null skips auto-assignment (the app still launches, it just isn't herded to its workspace). Find one with `osascript -e 'id of app "…"'`. Example: ```nix "com.tinyspeck.slackmacgap" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..barIcon` `null or string` · default `null` The SketchyBar workspace-pill glyph. A sketchybar-app-font ligature like ":slack:" renders the app's logo; any other string is drawn in the bar's Nerd Font. null falls back to the workspace letter. Ignored when workspace is null. Example: ```nix ":slack:" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..cask` `null or string` · default `null` Homebrew cask that installs this app. When set, it's appended to homebrew.casks so declaring the app also installs it. null means "already present / installed some other way" (e.g. Safari, Music). Example: ```nix "slack" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..enable` `boolean` · default `true` Whether this app participates in the shared launcher roster. Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..key` `string` · no default The leader letter for this app: tap Caps Lock then this key to launch/focus it. Must be unique across the roster. Example: ```nix "s" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..label` `null or string` · default `null` Cheatsheet caption for the leader key. null uses name. Example: ```nix "Slack" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..name` `string` · no default macOS application name, as passed to `open -a`. Example: ```nix "Slack" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..order` `signed integer` · default `1000` Roster order; lower values appear first. Ties are sorted by app id. Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.apps..workspace` `null or string` · default `null` The AeroSpace workspace this app owns — its window auto-moves here, it gets a SketchyBar pill, and the leader then ⇧ throws a window to it. null makes the app "launcher-only": the leader still opens it in the current workspace, but it claims no workspace, pill, or auto-assign rule (e.g. Passwords). Example: ```nix "S" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ## nebelhaus.theme Colour and wallpaper. ### `nebelhaus.theme.accent` `one of "rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "sapphire", "blue", "lavender"` · default `"mauve"` The accent colour, by Catppuccin name (the Nebelung palette is a grey-tinted Catppuccin, so the fourteen names are the same in both flavors — the hue you pick follows nebelhaus.theme.flavor). It recolours the tools nebelhaus injects colours into — lazygit, fzf, yazi, and the Zen browser — via the matching Nebelung per-accent ports. Honest scope: this moves the accent on those tools, NOT literally everything. Single-file dotfiles that bake the palette at their own theme slot (ghostty, starship, tmux, bat, zellij, …) keep their built-in colour and don't follow this option. The base palette stays the same Nebelung grey either way — only the accent hue changes. Example: ```nix "sapphire" ``` Declared in [`modules/theme/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/theme/options.nix). ### `nebelhaus.theme.contrast` `one of "normal", "high"` · default `"normal"` How far the interface separates from its background. "high" swaps in the Nebelung high-contrast palette: the same hues and the same accents, with the neutral ramp pulled apart in OKLCH so text and background separate further at every step. Measured rather than eyeballed — body text goes from 11.3:1 to 19.9:1 against the base, clearing WCAG AAA (nebelung's own CI asserts it). Composes with `flavor`, and the boost is tuned per flavor rather than shared: light mode has far less room above its background before the ramp clips to white, so latte goes 7.0:1 → 9.9:1 where mocha goes 11.3 → 19.9. Both keep all twelve ramp steps distinct, which is the property nebelung's tests actually assert. Honest scope. This recolours what the rice injects colours into: Ghostty, bat, delta, lsd, yazi, zellij, glow, starship, lazygit, the bar, pounce and trill (at runtime, via ~/.config/{pounce,trill}/themes/ — and unlike `flavor`, contrast reaches both on BOTH halves of their light/dark pair), Zen and Obsidian. It does NOT reach: - macOS itself. For system-wide contrast see nebelhaus.accessibility.increaseContrast — a separate, FDA-gated setting. The two are complementary, and a genuinely high-contrast machine wants both. Example: ```nix "high" ``` Declared in [`modules/theme/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/theme/options.nix). ### `nebelhaus.theme.flavor` `one of "mocha", "latte"` · default `"mocha"` Light or dark. "mocha" (the default) is the rice as it has always been; "latte" is light mode. Not an inversion of the dark palette — a different SOURCE palette. Nebelung is "Catppuccin with the blue stripped out", and those rules say nothing about dark, so they apply to Catppuccin Latte just as well: same warm-grey neutral ramp, same calmed accents, the other polarity. Light mode lands at 7.0:1 for body text on its own, so it's legible before you reach for contrast = "high" (which takes it to 9.9:1). It composes with `contrast`: the two axes give four palettes, and nebelung's CI measures each one's contrast ratio rather than eyeballing it. Honest scope, in two parts. What follows it: every tool the rice injects colours into or points at a rendered theme — Ghostty, bat, delta, lsd, yazi, fzf, glow, starship, lazygit, helix, zellij, opencode, the bar, Zen and Obsidian. These are genuinely re-rendered for the flavor, not recoloured in place: whiskers takes different branches for a light flavor (terminal ANSI 0/7/8/15 swap, Zen switches its prefers-color-scheme block, delta sets `light = true`). What does NOT follow it: - pounce and trill, by default. Both read their palette at runtime and can pick per polarity, so nebelhaus.pounce.followSystemAppearance and nebelhaus.trill.followSystemAppearance (default true) hand that choice to macOS Light/Dark instead: the rice installs every rendered variant into ~/.config/{pounce,trill}/themes/ and writes the dark/light PAIR at your `contrast`. Set either option false to pin that app to this flavor like everything else. - macOS's own Light/Dark appearance. Turning ON dark mode is one typed setting, but turning it OFF means DELETING a default rather than writing one, which nix-darwin has no way to express — so the rice leaves system appearance alone in both directions and you set it in System Settings ▸ Appearance. A latte rice on a dark macOS looks half-done, and that half is currently yours — except in pounce and trill, which read the appearance themselves. - the desktop wallpaper (nebelhaus.theme.wallpaper). The three hand-made looks have the dark palette baked in; only "bold" is generated, and it follows theme.accent rather than the flavor. Example: ```nix "latte" ``` Declared in [`modules/theme/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/theme/options.nix). ### `nebelhaus.theme.ports.enable` `boolean` · default `true` Theme the apps in your roster (`nebelhaus.apps`) that Nebelung ships a port for, without wiring each one by hand. The rice already themes every tool it installs itself — the shell, the terminal, the git stack, Zen, Obsidian. This covers the other direction: an app YOU added to the roster that Nebelung happens to have a theme for. Add `zed`, `warp` or `xcode` to `nebelhaus.apps` and its Nebelung theme lands where that app looks for themes, in the flavor and contrast you selected, following them on every rebuild. Matching is by roster id, so the entry has to be named after the port (`zed`, not `zed-editor`). Honest scope, and it is the whole point of the option: this drops the theme FILE. Whether that alone makes the theme *active* is the app's choice, not ours, and Nebelung records which is which per port. Ghostty reads a config key we own, so it just works. Xcode, Warp, OBS and friends offer no file interface for picking a theme — the file is put where they look, and the one click that selects it stays yours. `haus doctor` lists exactly which apps are waiting on that click, so the difference is visible rather than something you discover months later. Ports whose install is a merge into an existing config file, or that need a compile step first, are reported but never written: silently half-applying someone's config is worse than saying so. Declared in [`modules/theme/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/theme/options.nix). ### `nebelhaus.theme.wallpaper` `one of "none", "orbits", "constellation", "flow", "bold"` · default `"none"` The desktop wallpaper, set at each home-manager activation (osascript, every desktop on the current Space). Four Nebelung looks: orbits · constellation · flow hand-made, the palette baked in bold generated from theme.accent, so it follows the accent (a bold pink at accent = "pink") Default "none" leaves your current wallpaper alone — changing the desktop is visible and personal, so nothing moves unless you ask (the bootstrap interview offers the choice on a fresh install). Example: ```nix "orbits" ``` Declared in [`modules/theme/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/theme/options.nix). ## nebelhaus.fonts The terminal font. The bar keeps its own font at its own tuned sizes. ### `nebelhaus.fonts.mono.name` `string` · default `"JetBrainsMono Nerd Font Mono"` The terminal font family, as Ghostty's `font-family` names it. This should be a NERD FONT patched build: starship's prompt, lsd's icons, and yazi all draw with glyphs a stock font renders as tofu. If you change this, set `package` too — the rice can only install a font it's been given. Example: ```nix "Berkeley Mono" ``` Declared in [`modules/den/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/den/options.nix). ### `nebelhaus.fonts.mono.package` `null or package` · default `null` The package providing `name`. null (the default) installs the rice's own JetBrains Mono Nerd Font, which is what `name` defaults to. Set this whenever you change `name`, or the family simply won't exist on the machine and Ghostty will silently fall back — the rice warns if it spots that combination. Example: ```nix pkgs.nerd-fonts.fira-code ``` Declared in [`modules/den/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/den/options.nix). ### `nebelhaus.fonts.mono.size` `positive integer, meaning >0` · default `19, scaled by nebelhaus.ui.scale` Terminal font size in points. The single most useful knob for a larger-text machine, since it moves everything the rice actually lives in. 19 (at ui.scale = 1.0) is the base for a reason worth knowing: the Ghostty window is tiled to a fixed pixel height by prowl, and sizes that don't divide that height evenly used to leave a gap under zellij's status bar. That's since been fixed properly (window-padding-balance + `extend-always`), so any size is safe now — 19 is simply the tuned starting point. Example: ```nix 24 ``` Declared in [`modules/den/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/den/options.nix). ## nebelhaus.hearth The shell and terminal experience. ### `nebelhaus.hearth.editor` `string` · default `"hx"` The ONE editor the rice uses everywhere. It's the shell command for $EDITOR / $VISUAL (git, etc.) AND what every "open in an editor" action launches — the "Nix Config" palette command, the bar's nix-open item, and the file-association hijack. Those open the target in a new zellij tab running this command, so a terminal editor (hx, nvim, vim, nano) is the natural fit for the rice; a GUI editor's CLI works too (e.g. "code" or "code -w" to block). Example: ```nix "nvim" ``` Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ### `nebelhaus.hearth.hijackFileAssociations` `boolean` · default `false` When true, build a small opener app and make it the default handler for ~80 text/code extensions (json, md, ts, nix, rs, go, kdl, …), so opening or clicking those files opens them in nebelhaus.hearth.editor in a terminal tab. The app declares the types itself (not just `duti`) so extensions nothing else on the machine declares still bind. Off by default: silently rewriting your file associations is a jarring, hard-to-undo change, so it's strictly opt-in. (Extensionless executables like `bench` are NOT covered — macOS gates the public.unix-executable handler behind an interactive dialog; set it by hand once if wanted: `duti -s org.nebelhaus.editoropen public.unix-executable all`.) Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ### `nebelhaus.hearth.obsidianVaults` `list of string` · default `[ ]` Home-relative paths to existing Obsidian vaults that should use the Nebelung theme. On each activation, Hearth copies the rendered theme.css + manifest.json into each vault's .obsidian/themes/Nebelung/ directory, selects Nebelung's dark appearance in appearance.json, and removes the obsolete "nebelung" CSS snippet from the enabled list. Empty (the default) leaves every vault untouched. Paths must be relative to the user's home, may not contain "..", and are skipped with a warning unless their .obsidian directory already exists. Example: ```nix [ "Library/Mobile Documents/iCloud~md~obsidian/Documents/notes" ] ``` Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ### `nebelhaus.hearth.zellijStartLocked` `boolean` · default `true` When true (the default), zellij boots into Locked input mode instead of Normal — its single-key submode leaders (pane, tab, resize, …) stay inert until you unlock with Ctrl-g, so a stray keystroke can't jump you into a submode. The `Super`-prefixed launchers (claude / pane / tab / yazi-peek / fullscreen) are bound in `shared` and keep working while locked, as do `Alt [` / `Alt ]` (cycle swap layouts) — the rest of zellij's `Alt` row stays inert while locked, since those keys are readline/vim word motions the pane's app wants. The bar's bottom-right quick-hint block only shows in Locked mode. Set false to start in Normal mode (zellij's own default). Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ## nebelhaus.claude Claude Code integration. ### `nebelhaus.claude.globalMd` `strings concatenated with "\n"` · default `""` Contents of Claude Code's global memory file, written to ~/.claude/CLAUDE.md (hearth wires it into home-manager). This is your personal, cross-project operating context. When set, the rice prepends one short section of its own — the `wt child` worktree rule, since the rice ships `wt` and that rule is what keeps it working — then your text. Leave it empty to manage ~/.claude/CLAUDE.md fully by hand (nothing is written, so the rice never clobbers a by-hand file). Example: ```nix '' # CLAUDE.md — global How I like to work across every repo… '' ``` Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ### `nebelhaus.claude.skill` `boolean` · default `true` Install the `nebelhaus` Claude Code skill into ~/.claude/skills/nebelhaus, so an agent asked to "install Slack" or "make everything bigger" edits your host file and runs `haus rebuild` instead of guessing at dotfiles and `brew install`. The skill's option reference is GENERATED from the rice revision this machine is pinned to, so it can only ever describe options that actually exist here — and it is regenerated by `haus update`. It also carries this host's current state (which rooms are on, where the host file is) and a starter CLAUDE.md for your config repo. Unrelated to Claude Code's own settings, which follow nebelhaus.developer.agents.enable. This is a plain file drop: a machine that never runs an agent just carries an unread markdown file. Set false to leave ~/.claude/skills alone entirely. Declared in [`modules/hearth/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hearth/options.nix). ## nebelhaus.accessibility macOS accessibility keys the rice can actually apply. These write to a TCC-protected domain, so they take effect only when the app you run the rebuild from holds Full Disk Access — otherwise the rice warns and moves on. ### `nebelhaus.accessibility.differentiateWithoutColor` `null or boolean` · default `null` macOS's "Differentiate without colour" — native UI adds shapes and text where it would otherwise rely on hue alone. The setting to pair with a rice built for colour-blind readability. null (the default) leaves whatever you have alone — this is a personal setting, so the rice never picks a value for you. REACHABILITY: `com.apple.universalaccess` is TCC-protected. It writes only when the app that runs the rebuild holds Full Disk Access (System Settings ▸ Privacy & Security ▸ Full Disk Access; on macOS 26 a stale grant often needs removing and re-adding with (+)). Without that grant the rice logs a warning and moves on — it does NOT fail the rebuild. Worth knowing: an agent-driven `haus rebuild` runs under a different app than your terminal, so it may skip this while your own rebuild applies it. Example: ```nix true ``` Declared in [`modules/den/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/den/options.nix). ### `nebelhaus.accessibility.increaseContrast` `null or boolean` · default `null` macOS's "Increase contrast" — stronger borders and reduced use of colour alone to convey state, across native apps. This is the system-level companion to a high-contrast nebelhaus theme: the theme restyles the tools nebelhaus colours, this reaches everything else. null (the default) leaves whatever you have alone — this is a personal setting, so the rice never picks a value for you. REACHABILITY: `com.apple.universalaccess` is TCC-protected. It writes only when the app that runs the rebuild holds Full Disk Access (System Settings ▸ Privacy & Security ▸ Full Disk Access; on macOS 26 a stale grant often needs removing and re-adding with (+)). Without that grant the rice logs a warning and moves on — it does NOT fail the rebuild. Worth knowing: an agent-driven `haus rebuild` runs under a different app than your terminal, so it may skip this while your own rebuild applies it. Example: ```nix true ``` Declared in [`modules/den/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/den/options.nix). ## nebelhaus.prowl Tiling window management and the Caps-Lock leader launcher. ### `nebelhaus.prowl.enable` `boolean` · default `true` AeroSpace tiling window management + the leader-key launcher. This is the room switch: off drops AeroSpace, its launch agent, the wake-time window re-sort and the key remap entirely. To keep the tiler but leave the keyboard alone, use nebelhaus.keys.leader = "none" and nebelhaus.keys.windowNav = "none" instead of turning the room off. Declared in [`modules/prowl/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/prowl/options.nix). ## nebelhaus.sill The menu bar, and which pills it draws. ### `nebelhaus.sill.aiUsage.provider` `one of "latest", "claude", "codex", "opencode"` · default `"latest"` Which AI provider to display in the main pill: `latest` (default, automatically shows whichever provider reported most recently), or one of `claude`, `codex`, `opencode`. Clicking the pill always displays the full dropdown with all reporting providers. Note this is about *usage readouts*, not about which client `wt` can spawn: a provider reports here whenever it has data for your account — Codex notably does so from a ChatGPT login alone, with no CLI installed — so it is deliberately not tied to `nebelhaus.agents.clients`. Example: ```nix "claude" ``` Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.battery.hideOver` `null or signed integer` · default `null` Hide the battery pill when charge percentage is above this threshold (e.g., set to 80 to show the battery pill only when charge is at or below 80%). Example: ```nix 80 ``` Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.clock.mode` `one of "full", "compact"` · default `"full"` The display mode for the clock pill: `full` (default, e.g. "Fri Jul 31 09:41 AM" with calendar icon) or `compact` (e.g. "Fri 31/7 9:41" without icon and trimmed spacing). Example: ```nix "compact" ``` Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.enable` `boolean` · default `true` The SketchyBar menu bar. When off, the native macOS menu bar is kept (nebelhaus stops hiding it) and no bar is drawn. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items` `submodule` · default `{ }` Which SketchyBar pills to draw, one bool each. The core pills — `clock`, `weather`, `media`, `battery`, `wifi` — default true; the extras — the readouts `cpu`, `memory`, `volume`, `calendar`, `caffeinate` and the personal `agents`, `aiUsage`, `elgato`, `harvest` — default false. Set only what you want to change: nebelhaus.sill.items = { weather = false; # drop a default-on core pill cpu = true; # add an off-by-default readout caffeinate = true; # add the keep-awake controller }; A pill set false is never created (its update script doesn't run either). The hush (Do-Not-Disturb) pill is separate — it rides nebelhaus.hush.enable, not this set. Example: ```nix { cpu = true; weather = false; } ``` Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.agents` `boolean` · default `false` A paw pill tracking your agent-worktree panes — amber when one is blocked on you, click for the per-agent list, each row marked with the client sitting in it; left-click a row to jump to that pane, ⌥/right-click for a live `zellij subscribe` peek. Fed by each client's own lifecycle hooks, which all call `agent-state` (also installed as ~/.config/sketchybar/plugins/agents-hook.sh): Opencode's plugin and Codex's ~/.codex/hooks.json are written for you (Codex asks you to trust its hooks the first time it sees them), while Claude Code's four hooks stay yours to point at it in ~/.claude/settings.json — Claude owns that file and rewrites it, so the rice never touches your hooks. A row whose zellij pane is gone drops off by itself, which is what stands in for the session-end event Codex doesn't have. Dormant until a client fires. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.aiUsage` `boolean` · default `false` A gauge pill showing AI usage (Claude Code/Codex subscription rate limits as %, or Opencode API token cost as daily $). Automatically shows whichever provider reported most recently. Click for expanded session/weekly limits and daily/monthly API costs with model breakdowns. Claude and Opencode are read off disk; Codex has no local usage data, so its row is polled from your ChatGPT account with the OAuth token in ~/.codex/auth.json (refreshed and rewritten in place) — no Codex login on the machine, no call is made. Claude's row is pushed by its statusline; the Codex and Opencode rows are pulled by the pill itself on a 3-minute TTL, so they stay current on a machine that never opens Claude at all. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.battery` `boolean` · default `true` The battery pill. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.caffeinate` `boolean` · default `false` A coffee pill that prevents idle system sleep for 1/2/4/8 hours, a custom whole-hour duration, or indefinitely. The display may still turn off; closing a MacBook lid still sleeps it. Uses macOS's built-in `caffeinate`, so there is no extra package. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.calendar` `boolean` · default `false` Your next timed event, with a click-popup of the next five. Pulls in `ical-buddy` automatically and reads Calendar, so macOS prompts for Calendar access on first run. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.claudeUsage` `boolean` · default `false` Deprecated alias for `aiUsage`. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.clock` `boolean` · default `true` The clock pill, pinned to the far right. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.cpu` `boolean` · default `false` Total CPU load, as a percentage pill. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.elgato` `boolean` · default `false` Toggles an Elgato Key Light on the local network. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.harvest` `boolean` · default `false` A Harvest time-tracking pill; needs a ~/.config/sketchybar/harvest_secrets.sh you provide. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.media` `boolean` · default `true` The now-playing track (scrolls; auto-hides when nothing plays). Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.memory` `boolean` · default `false` Memory-pressure percentage pill. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.volume` `boolean` · default `false` Output volume / mute state. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.weather` `boolean` · default `true` The weather pill and its click-to-open forecast popover. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.items.wifi` `boolean` · default `true` The Wi-Fi status pill. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.sill.position` `one of "top", "bottom", "auto"` · default `"top"` Where the bar sits. `top` and `bottom` pin it there. `auto` flips it at runtime — `bottom` whenever an external display is attached (docked with the lid open, or clamshell), `top` on the built-in display alone — driven by a `display_change` hook, so the bar moves the moment you dock or undock, without a rebuild. The bar's height/pill offsets are tuned for the notch, which only exists at the top of the built-in display; at `bottom` there's no notch to tuck under, so `auto` conveniently keeps the notch case (`top`) on the notched screen and the plain case (`bottom`) on the external. Example: ```nix "auto" ``` Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ## nebelhaus.tour The first-run tutor. ### `nebelhaus.tour.enable` `boolean` · default `true` The haus tour — a first-run tutor that walks the four moves (launch / navigate / resize / palette) as ONE quiet pill in the bar, advancing live as each move is detected. It never opens a window or steals focus: a fresh machine just shows a dormant "new here?" hint, clicking it (or `haus tour`, or ⌘Space → tour) starts the lap, right-click hides it forever. Detection reuses signals the rice already fires (the leader-mode scripts) — no key logging, no Accessibility. Needs prowl + sill (it silently stays out of the bar without them); the ⌘Space step is dropped when pounce is off. Progress lives in ~/.local/state/nebelhaus — `haus tour reset` re-arms a finished tour. Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.tour.steps` `null or (non-empty (list of (submodule)))` · default `null` A community-authored tour, in order. null keeps the built-in four-move nebelhaus tour unchanged; supplying a list replaces it, so a shared rice can teach its own workflow without shipping scripts or reaching outside the `nebelhaus.*` option surface. Detection reuses signals the rice already emits. `launch`, `workspace`, `navigate` and `resize` need prowl; `palette` needs Pounce and its palette binding. The module warns when a chosen detector's room is disabled. Example: ```nix [ { detect = "palette"; hint = "Press ⌘Space, type tour, then hit ↵"; } ] ``` Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.tour.steps.*.detect` `one of "launch", "workspace", "navigate", "resize", "palette"` · no default The existing rice signal that completes this step: entering launch, navigate or resize mode; changing workspace; or running the Haus Tour command from Pounce (`palette`). The tour observes outcomes, never keystrokes. Clicking the pill still skips a step that cannot be detected in the current setup. Example: ```nix "palette" ``` Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ### `nebelhaus.tour.steps.*.hint` `string` · no default The instruction shown in the tour pill for this step. Example: ```nix "Press ⌘Space, type calendar, then hit ↵" ``` Declared in [`modules/sill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/sill/options.nix). ## nebelhaus.pounce The ⌘Space command palette. ### `nebelhaus.pounce.enable` `boolean` · default `true` The pounce command palette daemon (⌘Space) + its rice commands. Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ### `nebelhaus.pounce.followSystemAppearance` `boolean` · default `true` Let the palette follow macOS Light/Dark Mode instead of pinning one polarity: pounce gets the nebelung variant AND its latte counterpart at your nebelhaus.theme.contrast, as its `theme`/`themeLight` pair, and picks between them per open (no rebuild, no daemon restart). Honest scope: this makes pounce the one themed tool that does NOT follow nebelhaus.theme.flavor — a flavor pin is a *palette* choice, and asking to follow the system says the polarity is macOS's call. The contrast axis still applies to both halves. Everything else on the rice keeps whatever flavor pins. false pins pounce to the flavor like every other port, which is exactly what it did before this option existed. Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ### `nebelhaus.pounce.items` `attribute set of (submodule)` · default `{ }` Per-item palette settings, keyed by the item's own address. One entry is one row of the palette: hide it, give it a search shorthand, give it a key. "cmd:" a command, by script name without .sh "app:/Applications/Foo.app" an application, by path "mode:" a built-in window — launcher, clipboard, emoji, screenshots, camera, filesearch Those keys are pounce's own address space (the same strings its frecency store and `pounce run` use), so a key written here is also what you'd type to invoke the thing from a script or another tool's binding. Hotkeys can be a single chord ("opt+e") or a LEADER SEQUENCE — steps separated by spaces, modifiers by "+", the notation Emacs and VS Code use: hotkey = "opt+space e"; # ⌥Space, then E hotkey = [ "cmd+k" "cmd+c" ]; # the same thing, step by step Sequences are worth knowing about on a tiling rice: they open a namespace that structurally can't collide with the ⌥/⌘ chords prowl already claims, and they need no Accessibility grant (pounce grabs the second step as an ordinary global hotkey for a couple of seconds rather than tapping events). Two things this checks at build time, because both fail SILENTLY at runtime: a key that names no real item shape (a "mode:" typo binds nothing at all), and a chord already claimed by nebelhaus.keys.palette or nebelhaus.keys.leader (whoever registers first wins, and it isn't always the same one). What it can't check is whether `cmd:` names a command that exists — command scripts are discovered at runtime, so pounce warns about that itself when the daemon starts, and `pounce doctor` lists any binding that failed to arm. Example: ```nix { "app:/Applications/Ghostty.app" = { hotkey = "opt+t"; }; "cmd:brew-services" = { listed = false; }; "cmd:emoji" = { alias = "emo"; hotkey = "opt+e"; }; "mode:clipboard" = { hotkey = "cmd+shift+v"; }; } ``` Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ### `nebelhaus.pounce.items..alias` `null or string` · default `null` A search shorthand, matched at a bonus over the item's real name — so "emo" can find the Emoji Picker without renaming it. Example: ```nix "emo" ``` Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ### `nebelhaus.pounce.items..caption` `null or string` · default `null` How this item reads on the cheatsheet page that lists your item hotkeys (⌘Space then ⇥, or the leader's `/`). Only used when the item has a `hotkey` — a row without a key has nothing to teach. Defaults to a name derived from the key, which is right often enough to leave alone: `mode:clipboard` becomes "Clipboard history", `app:/Applications/Ghostty.app` becomes "Ghostty", and `cmd:brew-services` becomes "Brew services". Set this when the derived name isn't what the palette actually calls the row — the rice can't read a command's own `# pounce: name` header at evaluation time, so that one is a guess. Example: ```nix "Clipboard history" ``` Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ### `nebelhaus.pounce.items..hotkey` `null or string or list of string` · default `null` A global chord, or a leader sequence, that invokes this item directly without opening the palette first. Modifier names follow pounce's spelling: cmd/command/super/meta · opt/option/alt · ctrl/control · shift. Whether the KEY name is one pounce can bind is not checked here (that vocabulary lives in the app); a chord it can't register is reported by `pounce doctor` rather than silently dropped. Example: ```nix "opt+space e" ``` Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ### `nebelhaus.pounce.items..listed` `boolean` · default `true` Whether the item appears in the palette's list. Named `listed` rather than `enable` because that is precisely what it does: false removes the ROW, and a `hotkey` on the same item keeps working. It's how you hide a command you only ever want to reach by key — or clear the launcher of tools someone else on this Mac has no use for, which is the closest thing to a "pack" the surface has today. (It writes pounce's own `enabled` key.) Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ### `nebelhaus.pounce.signingIdentity` `string` · default `""` A code-signing identity in your login keychain — either its SHA-1 or (preferred) its full common name. The pounce daemon is re-signed with it so a macOS Accessibility (TCC) grant survives rebuilds. List yours: security find-identity -v -p codesigning Prefer a "Developer ID Application" identity passed BY NAME (e.g. "Developer ID Application: Jane Doe (TEAMID)"): its designated requirement anchors on the stable team OU, so the grant survives even a certificate renewal (the renewed cert keeps the same name/team but gets a new SHA — a hardcoded SHA would silently fall back to unsigned). This is also the identity the Homebrew build is signed with, so both install paths share one identity. An "Apple Development" cert works too but expires yearly and pins the specific cert, so it's less durable. Changing this once invalidates the existing grant (the requirement changes) — re-approve pounce in Accessibility a single time after. Leave empty to run pounce unsigned (the palette works, but auto-paste and other Accessibility-gated features stay off). Example: ```nix "Developer ID Application: Jane Doe (ABCDE12345)" ``` Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ### `nebelhaus.pounce.windowSwitcher` `boolean` · default `true` Replace the stock ⌘Tab app switcher with pounce's MRU *window* switcher: tap ⌘⇥ to toggle to the last window (across workspaces), hold ⌘ and keep tapping ⇥ to walk older ones, type while holding to fuzzy-filter (frecency-ranked). Rows carry the window's AeroSpace workspace, and focusing goes through `aerospace focus --window-id` so a window parked on another workspace surfaces correctly. Needs the daemon to hold an Accessibility grant — in practice, set nebelhaus.pounce.signingIdentity so the grant survives rebuilds. Without the grant the event tap can't install and stock ⌘Tab keeps working, so this default is safe on a fresh, not-yet-granted install. false leaves ⌘Tab native even when the grant is there. Declared in [`modules/pounce/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/pounce/options.nix). ## nebelhaus.trill The Messages client. ### `nebelhaus.trill.enable` `boolean` · default `true` The trill Messages client, installed via the trill flake (copied to /Applications). Declared in [`modules/trill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/trill/options.nix). ### `nebelhaus.trill.followSystemAppearance` `boolean` · default `true` Let trill's palette follow macOS Light/Dark Mode instead of pinning one polarity: trill gets the nebelung variant AND its latte counterpart at your nebelhaus.theme.contrast, and picks between them itself — no rebuild, no relaunch. Same honest scope as the pounce option of the same name: with this on, trill does NOT follow nebelhaus.theme.flavor, because asking to follow the system says the polarity is macOS's call. The contrast axis still applies to both halves. Set it false to pin trill to theme.flavor like every other themed tool. Either way this writes only the DEFAULT: a palette chosen in trill's own Settings ▸ Theme wins over what the rice writes, and so does trill's appearance preference (Follow macOS / Dark / Light), which lives in its settings rather than here. Declared in [`modules/trill/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/trill/options.nix). ## nebelhaus.hush One quiet switch: Do Not Disturb, optional Slack status, and your hooks. ### `nebelhaus.hush.enable` `boolean` · default `true` The hush room: one quiet switch — bar pill, palette command, and a `hush` CLI — that turns macOS Do Not Disturb on/off (via the declaratively-bound symbolic hotkey 175, pressed synthetically), optionally sets your Slack status, and runs your hooks. Honest scope: hush flips the built-in Do Not Disturb, not named Focus modes, and it doesn't manage which apps break through — curate that once in System Settings. The keypress needs an Accessibility grant on whatever app invokes hush (palette runs inherit pounce's; grant sketchybar once for the pill). `hush doctor` walks the one-time steps. Declared in [`modules/hush/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hush/options.nix). ### `nebelhaus.hush.hooks` `list of (absolute path or string)` · default `[ ]` Extra scripts run on every hush/unhush, each called with a single argument "on" or "off". Paths are copied into the store; strings are run as-is (so $HOME paths work). Failures are logged, never fatal — a broken hook can't wedge the toggle. Example: ```nix [ ./onair-light.sh "/Users/ada/bin/pause-music" ] ``` Declared in [`modules/hush/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hush/options.nix). ### `nebelhaus.hush.slack.enable` `boolean` · default `false` Also set a Slack status and snooze Slack notifications (all devices, phone included) while hushed. Off by default: it needs a personal Slack user token (scopes users.profile:write + dnd:write) provided via tokenCommand. The previous status is saved and restored on unhush. Declared in [`modules/hush/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hush/options.nix). ### `nebelhaus.hush.slack.snooze` `boolean` · default `true` Also pause Slack's own notifications (dnd.setSnooze) while hushed — this is what silences the phone. Ended on unhush; capped at 24h as a failsafe if you forget. Declared in [`modules/hush/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hush/options.nix). ### `nebelhaus.hush.slack.statusEmoji` `string` · default `":no_bell:"` Slack status emoji while hushed. Declared in [`modules/hush/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hush/options.nix). ### `nebelhaus.hush.slack.statusText` `string` · default `"heads down"` Slack status text while hushed. Declared in [`modules/hush/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hush/options.nix). ### `nebelhaus.hush.slack.tokenCommand` `string` · default `""` Shell command that prints the Slack user token (xoxp-…) to stdout. Keychain-first so no secret ever lands in the store or a dotfile: security add-generic-password -s hush-slack -a $USER -w 'xoxp-…' Example: ```nix "security find-generic-password -s hush-slack -w" ``` Declared in [`modules/hush/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/hush/options.nix). ## nebelhaus.snippets Text expansion via espanso. ### `nebelhaus.snippets.enable` `boolean` · default `false` Text expansion via espanso: type a short trigger (say "@@") and it's replaced inline with a longer string (your email), in any app — browsers, Messages, and the terminal. espanso injects keystrokes, so it works where macOS's own text replacement doesn't (terminals, many Electron apps). Off by default: it installs the Espanso.app cask and needs a one-time macOS Accessibility grant (System Settings → Privacy & Security → Accessibility → enable Espanso) the first time it runs. The rice runs the SIGNED app bundle rather than a nix-store binary on purpose, so that grant is keyed to a stable identity and survives reboots and nixpkgs bumps — you grant it once, not on every rebuild (and so the espanso troubleshooting window stops popping up at login, since that window only ever meant "the grant went missing"). Declared in [`modules/snippets/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/snippets/options.nix). ### `nebelhaus.snippets.matches` `list of (submodule)` · default `[ ]` The expansion table — one { trigger; replace; } per snippet, written to ~/.config/espanso/match/default.yml. Only espanso's plain trigger→replace form is exposed here; for dynamic matches (dates, shell output, forms) drop a hand-written .yml alongside it in ~/.config/espanso/match/ — espanso loads every file in that dir. Example: ```nix [ { trigger = "@@"; replace = "ada@example.com"; } { trigger = "##"; replace = "+1 555 0100"; } ] ``` Declared in [`modules/snippets/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/snippets/options.nix). ### `nebelhaus.snippets.matches.*.replace` `string` · no default What it expands to. Example: ```nix "ada@example.com" ``` Declared in [`modules/snippets/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/snippets/options.nix). ### `nebelhaus.snippets.matches.*.trigger` `string` · no default What you type. Example: ```nix "@@" ``` Declared in [`modules/snippets/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/snippets/options.nix). ## nebelhaus.secrets Where secret values come from on this machine. ### `nebelhaus.secrets.provider` `null or string` · default `"keyring"` The secretspec provider that supplies secret VALUES on this machine. The secrets room writes it to ~/.config/secretspec/config.toml as the default provider, so `secretspec run / check / set` work without flags. Any provider string secretspec accepts, URIs included: "keyring" (macOS login keychain — local, no accounts), "onepassword", "bws" (Bitwarden Secrets Manager), "gcsm" (Google Cloud Secret Manager), "awssm" (AWS Secrets Manager), "vault", "pass", "protonpass", "lastpass", "dotenv", "env", or a scoped URI like "onepassword://account@vault". WHICH secrets exist is not declared here — that's each project's committed secretspec.toml. Cloud providers authenticate with their own credentials, configured outside Nix (e.g. `gcloud auth application-default login` for gcsm); that login is the one manual step on a new Mac. null skips writing the config file entirely — run `secretspec config init` yourself. Example: ```nix "gcsm" ``` Declared in [`modules/secrets/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/secrets/options.nix). ## nebelhaus.homebrew How rebuilds treat Homebrew packages you did not declare. ### `nebelhaus.homebrew.autoUpdate` `boolean` · default `false` Run `brew update` before activating the Homebrew step on every rebuild. Off by default — reproducible rebuilds shouldn't silently pull newer formulae. Turn on if you want brew to track upstream. Declared in [`modules/den/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/den/options.nix). ### `nebelhaus.homebrew.cleanup` `one of "none", "uninstall", "zap"` · default `"none"` How `darwin-rebuild switch` treats Homebrew casks/brews that are installed but NOT declared anywhere in your config. - "none" (default, safe): leave undeclared formulae/casks alone. The rice never deletes apps you installed yourself. - "uninstall": remove undeclared formulae/casks (keeps their data). - "zap": remove undeclared formulae/casks AND their app data. Fully declarative, but a stray cask you forgot to list is deleted — with no backup — on the very next rebuild. Only choose this once every app you keep is declared (bootstrap can adopt your current casks). Declared in [`modules/den/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/den/options.nix). ### `nebelhaus.homebrew.upgrade` `boolean` · default `false` Upgrade outdated Homebrew packages on every rebuild. Off by default for the same reproducibility reason as autoUpdate. Declared in [`modules/den/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/den/options.nix). ## nebelhaus.agents ### `nebelhaus.agents.clients` `list of (one of "claude", "codex", "opencode")` · see below Which coding-agent clients to install. `claude` is Claude Code, `codex` is OpenAI Codex, `opencode` is OpenCode. The ⌘C terminal binding starts whichever one `agents.default` names — Claude Code through its own `--worktree` hook, the others through `wt new`. A list rather than one bool per client, matching `developer.languages` — a fourth client later doesn't change this option's shape. This is the option that makes `agents.default` honest. Naming a client you have not installed used to fail *at spawn time*, inside the pane, after the worktree already existed: a flash of `codex is unavailable`, and litter to reap. `agents.default` must now be a member of this list, so the same mistake fails the rebuild instead, with both values named. Override the package for a client the usual Nix way — an overlay on `claude-code`, `codex` or `opencode` — rather than dropping the client here and installing your own copy alongside; two derivations shipping the same `bin/` name collide in one profile. Example: ```nix [ "claude" "codex" ] ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.agents.default` `one of "claude", "codex", "opencode"` · default `"claude"` The coding agent started by Pounce's **Spawn Agent** commands, by the ⌘C / Super-c zellij binds and the `c` shell alias, and used to reopen worktrees with no client recorded yet. Each spawned worktree records its own client, so changing this affects new work but never reopens an existing Codex or OpenCode task in Claude. Must be one of `agents.clients` — see there. Only `claude` can make its own worktree (its native `--worktree` flag, which fires the `wt` create hook); for `codex` and `opencode` ⌘C runs `wt new` instead, producing the same checkout, branch and registry entry from the outside. Resuming follows the client too: `codex` reopens its cwd-filtered `codex resume` picker, `opencode` continues its latest session for that cwd. All three share one `wt` branch/parking/reap lifecycle, and all three light up the `agents` bar pill and the zellij tab-bar badge — the opencode plugin and the codex hooks are written for you; only Claude Code's stay yours to wire, because Claude owns its own settings.json (see `nebelhaus.sill.plugins`). Example: ```nix "codex" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ## nebelhaus.developer ### `nebelhaus.developer.agents.enable` `boolean` · default `config.nebelhaus.developer.enable` Coding-agent *tooling*: `wt` (agent worktrees), `agent-state` (the pane-status writer behind the `agents` bar pill and the zellij tab badge), `zscratch`, the agent-worktree statusline, and the client config hearth writes (Claude Code's settings.json keys, opencode's agent-state plugin). Which clients get installed is `agents.clients`. Off is right for any machine not running coding agents — it's a large surface a non-developer never sees. It also empties `agents.clients`, since a client with no `wt` to park it is not the deal on offer. Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.developer.enable` `boolean` · default `true` The developer pack: the CLI toolbelt, Git tooling, coding-agent tooling, and language runtimes. On (the default) is the rice as it has always been. `false` is what makes a non-developer nebelhaus possible — it strips those tools rather than merely hiding them. What remains is the product: `haus`, `awake`, the theme, the terminal, the bar, the tiler and the palette. The sub-options below each default to THIS value, so turning it off turns everything off and you can then re-enable one piece: nebelhaus.developer.enable = false; nebelhaus.developer.git.enable = true; # …but keep git Example: ```nix false ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.developer.git.enable` `boolean` · default `config.nebelhaus.developer.enable` Git and its surroundings: the shell alias vocabulary, the themed git config, delta (diff pager), lazygit, `gh`, and gnupg for commit signing. Off drops all of them, and `nebelhaus.git.*` then has nothing to configure. Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.developer.languages` `list of value "node" (singular enum)` · default `[ "node" ] when developer.enable is true, else [ ]` Language runtimes to install. Currently only "node" (bun + fnm, with fnm's `--use-on-cd` shell hook). Deliberately a list rather than one bool per language, so adding "rust" or "python" later doesn't change this option's shape. Example: ```nix [ ] ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.developer.toolbelt.enable` `boolean` · default `config.nebelhaus.developer.enable` The terminal toolbelt: bat, fzf, fd, yazi, zoxide, lsd, glow, jq, tree, chafa, ttyd and fastfetch — the themed replacements for cat, find, ls and friends that the rice's shell is built around. Off leaves a plain shell. The prompt (starship) and the colour scheme stay: these are the *tools*, not the appearance. Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ## nebelhaus.displays ### `nebelhaus.displays` `attribute set of (submodule)` · default `{ }` Per-display settings, keyed by which screen you mean: internal the built-in panel main whichever display is currently main a persistent display UUID, for a specific external monitor — run `hausdisp list` to print the UUIDs of what's attached Default is the empty set, and then nothing about your displays is touched. A key naming a display that isn't plugged in right now is skipped with a note, not an error, so a `displays.` entry for the monitor at the office can't fail a rebuild on the train. Why this option exists at all: display scaling is the only lever macOS 26 gives us for "make EVERYTHING bigger", system-wide, including apps the rice knows nothing about. macOS's own text-size setting writes a value no running app re-reads, while the accessibility scalars that do work affect contrast or motion rather than system-wide size — measured, not assumed (the workshop's notes/macos-settings-matrix.md records the sweep). So `nebelhaus.ui.scale` and `nebelhaus.fonts` make the *rice* bigger, and this makes the *Mac* bigger. Example: ```nix { "37D8832A-2D66-02CA-B9F7-8F30A301B230" = { uiScale = "more-space"; }; internal = { uiScale = "larger-text"; }; } ``` Declared in [`modules/displays/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/displays/options.nix). ### `nebelhaus.displays..uiScale` `null or one of "more-space", "default", "larger-text", "largest-text"` · default `null` The scaled resolution, as an intent rather than a pixel count — the same four positions System Settings ▸ Displays offers, named: more-space the largest resolution the panel offers (smallest UI) default the panel's own default mode larger-text between the default and the smallest resolution largest-text the smallest resolution the panel offers (biggest UI) Resolved per panel from the modes that panel actually reports, so the same value means the same *thing* on a 14" laptop and a 27" monitor rather than the same number of pixels. On the 14" MacBook Pro this was developed on that resolves to 1800x1169 · 1512x982 · 1147x745 · 1024x665. Applied at each home-manager activation and set permanently, so it survives a reboot; re-applying an already-current mode is a no-op, so a rebuild doesn't flash your screen. null (the default) leaves the display alone. When more than one selector names the same attached panel, the more specific setting wins: UUID over internal over main. This lets a host-specific display setting refine a broad preset such as large-print without depending on activation order. Honest scope: this is a real, system-wide size change — every app gets bigger, not just the rice's own tools — and the cost is desk space, because a larger UI means less of it. It also can't run from a rebuild with no GUI session attached (over SSH, say); the setting applies at the next activation you run while logged in. Example: ```nix "larger-text" ``` Declared in [`modules/displays/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/displays/options.nix). ## nebelhaus.keys ### `nebelhaus.keys.leader` `one of "caps", "alt-space", "none"` · default `"caps"` What enters the launcher/leader mode — tap it, then a letter opens an app, a digit focuses a workspace, ⇧+either throws the focused window to that workspace and follows it there, an arrow navigates, `-`/`=` resizes. - "caps" (default): Caps Lock. AeroSpace can't bind Caps Lock itself, so the rice remaps it to F18 with hidutil and binds that. - "alt-space": the leader without giving up Caps Lock. No remap at all. - "none": no leader. Caps Lock stays Caps Lock, launch mode is unreachable, and nothing is remapped — the setting for a mouse-first rice, or for a Mac you are handing to someone else. What the leader fronted is still reachable: apps through the palette, window moves through service mode's join-with and the palette's own commands. Workspace focus and the workspace throws go away with it — they live only in launch mode. The remap is re-applied at every activation and does not survive a reboot, so moving off "caps" ends it — at the latest, at next boot. Only meaningful with nebelhaus.prowl.enable (AeroSpace owns the modes). Example: ```nix "none" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.keys.leaderExtras` `list of (submodule)` · default `[ ]` Extra launch-mode (leader) bindings beyond the app roster: tap the leader, then `key`, to run `command`. Use it for leader actions that aren't "launch an app" — a script, an AppleScript, opening a URL. Only meaningful with nebelhaus.prowl.enable and keys.leader != "none" (with no leader there is no launch mode to bind into). Example: ```nix [ { key = "enter"; command = "osascript -e 'tell application \"Things3\" to show quick entry panel'"; caption = "Things Quick Entry"; } ] ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.keys.leaderExtras.*.caption` `null or string` · default `null` The Launch Mode cheatsheet caption for this action. null falls back to the raw command, which is rarely what you want — set it. Example: ```nix "Things Quick Entry" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.keys.leaderExtras.*.command` `string` · no default The shell command run when the leader is followed by `key`; launch mode exits afterward. It's written verbatim into a small `/bin/sh` script that AeroSpace execs, so ordinary shell rules apply — `$HOME` resolves, and single quotes (an `osascript -e '…'`, say) are safe, which they would not be inlined into AeroSpace's own config. Example: ```nix "osascript -e 'tell application \"Things3\" to show quick entry panel'" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.keys.leaderExtras.*.key` `string` · no default The AeroSpace key name pressed after the leader (e.g. "enter", "space", "period", or a letter). Must not collide with a roster app's key or a built-in launch-mode key (the digits 1-4, the arrows, `-`/`=`, `v`/`e`/`z`, `,`, `` ` ``, `/`, esc) — nor with the workspace throws, which are ⇧ + any of those digits or a roster letter ("shift-1", "shift-b", …). An assertion in modules/prowl catches a clash rather than letting one binding silently shadow another. Example: ```nix "enter" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.keys.palette` `one of "cmd-space", "alt-space", "ctrl-space", "none"` · default `"cmd-space"` What opens the pounce command palette. Registered in-process by the daemon, so it's near-instant and doesn't go through AeroSpace. "cmd-space" (default) is the one value that also DISABLES Spotlight's own ⌘Space, because the two can't share it. Every other value leaves Spotlight alone — including "none", which hands the palette's job back to Spotlight entirely. That's a fix as much as an option: the rice used to take Spotlight's ⌘Space away unconditionally, even where nothing claimed it. Only meaningful with nebelhaus.pounce.enable. Example: ```nix "none" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ### `nebelhaus.keys.windowNav` `one of "alt", "ctrl-alt", "cmd-alt", "none"` · default `"alt"` The modifier vocabulary for prowl's window chords — one setting rather than a bind-per-action, because what people need to move is the modifier, not the letters. It drives focus (`` + hjkl), layouts (`` + `/` `,`), fullscreen, workspace back-and-forth, moving a workspace to the next monitor (`⇧⇥`), and entering service mode (`⇧;`). Anything that names a workspace — focusing one, or throwing the focused window there — hangs off `leader` instead, not this option. "alt" (default) is ⌥. The alternatives are for **non-US keyboard layouts**, where ⌥+letter types accented characters — a rice that owns ⌥+letter is unusable on those, which is the concrete reason this option exists. Whatever you pick, AeroSpace claims those chords **globally**, so they stop reaching whatever owned them inside a terminal. The surface is small now that the workspace throws moved to the leader: only hjkl, `/` `,`, `f`, `⇥`, `⇧⇥` and `⇧;`, none of which a roster letter can land on. (Under "ctrl-alt" that used to bite — the throws were `⌃⌥⇧` + an app's roster letter, so an app on `c` silently ate hearth's zellij `Ctrl Alt Shift c` in-place-agent bind. That collision is gone.) Nothing on a stock macOS collides either: the only ⌃⌥ system hotkeys are input-source switching (⌃⌥Space, off by default) and hyper-F13. "none" drops the modifier chords entirely: no focus/layout chords, no service mode. Combined with `leader = "none"` that's a rice where the tiler tiles and the keyboard is left alone — mouse-first. The cheatsheet follows, so it never advertises a key that does nothing. Only meaningful with nebelhaus.prowl.enable. Example: ```nix "ctrl-alt" ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). ## nebelhaus.perch ### `nebelhaus.perch.enable` `boolean` · default `true` The perch notch file shelf, installed via the perch flake (copied to /Applications). Declared in [`modules/perch/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/perch/options.nix). ### `nebelhaus.perch.followSystemAppearance` `boolean` · default `true` Let the shelf's palette follow macOS Light/Dark Mode instead of pinning one polarity: perch gets the nebelung variant AND its latte counterpart at your nebelhaus.theme.contrast, and picks between them itself — no rebuild, no relaunch. Same honest scope as the trill and pounce options of the same name: with this on, perch does NOT follow nebelhaus.theme.flavor, because asking to follow the system says the polarity is macOS's call. The contrast axis still applies to both halves. Set it false to pin the shelf to theme.flavor like every other themed tool. Perch has no theme picker of its own — the shelf is a five-second surface with nowhere to put one — so unlike trill, this is the only word on its colors. Declared in [`modules/perch/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/perch/options.nix). ## nebelhaus.ui ### `nebelhaus.ui.scale` `integer or floating point number between 0.5 and 3.0 (both inclusive)` · default `1.0` One number for "make the interface bigger". 1.0 is the rice as tuned; 1.35 is a comfortable large-print setting; below 1.0 tightens things up. It sets the DEFAULT of the sizes it drives, so anything you pin by hand still wins: nebelhaus.ui.scale = 1.5; # everything grows nebelhaus.fonts.mono.size = 18; # …except the terminal, pinned here What it currently moves: - the terminal font size (nebelhaus.fonts.mono.size) - the Dock icon size (system.defaults.dock.tilesize) - prowl's window gaps What it deliberately does NOT move: - Sill's menu bar. Its height is tuned to sit inside the macOS menu-bar band so the hover-reveal covers it exactly; scaling that linearly breaks the alignment rather than making it bigger. The bar needs its own sizing pass, not a multiplier. - anything outside nebelhaus. macOS has no system-wide UI scale, so third-party apps follow only a display-resolution change. Example: ```nix 1.35 ``` Declared in [`modules/options.nix`](https://github.com/nebelhaus/nebelhaus/blob/main/modules/options.nix). --- # Keybindings cheatsheet Source: https://nebelhaus.com/reference/keybindings/ Every default shortcut across tiling, the launcher, the terminal, and Pounce — in one place. Every default binding in nebelhaus, grouped by where it lives. The same list is available live in-system: tap **⇪** then **/** to open Pounce's cheatsheet, generated from your actual app roster. Notation: `⌥` Option/Alt · `⌘` Command · `⌃` Control · `⇧` Shift · `⇪` Caps-Lock. These are the **defaults**. The three keys everything else hangs off are options — `nebelhaus.keys.leader` (what `⇪` is), `nebelhaus.keys.windowNav` (what `⌥` is) and `nebelhaus.keys.palette` (what opens Pounce) — so on a rice that has changed them, read `⇪` and `⌥` below as "the leader" and "the nav modifier". Any of the three can be `"none"`, which removes its bindings rather than moving them. See [rebinding](/guides/window-management/#not-fond-of-these-keys); your own live cheatsheet always reflects the keys you actually have. ## Tiling — main mode (prowl) | Keys | Action | |---|---| | `⌥H` `⌥J` `⌥K` `⌥L` | Focus left / down / up / right (Vim-style twin of ⇪ + arrows) | | `⌥/` | Tiles layout (toggles horizontal ↔ vertical split) | | `⌥,` | Accordion layout (toggles horizontal ↔ vertical) | | `⌥F` | Toggle fullscreen | | `⌥Tab` | Previous workspace (back-and-forth) | | `⌥⇧Tab` | Move workspace to next monitor | | `⌥⇧;` | Enter service mode | | `⌘Space` | Open Pounce | ## Launch mode — tap ⇪ (prowl) | Keys | Action | |---|---| | `` | Launch / focus that app (`T` terminal, `B` browser by default) | | `1`–`4` | Focus workspace 1–4 | | `⇧1`–`⇧4` | Throw the focused window to workspace 1–4 and follow it there | | `⇧` | Throw the focused window to that app's workspace and follow it there | | `←↓↑→` | Focus tiled window; drops into **navigate mode** (arrows repeat, `⇧`+arrow *moves* the window, `Esc`/`Return` exits) | | `-` / `=` | Enter resize mode (shrink / grow) | | `V` | Clipboard history (Pounce) | | `E` | Emoji picker (Pounce) | | `Z` | Reopen the last closed app (the ⌘⇧T analog) | | `,` | Open macOS System Settings (mirrors the ⌘, convention) | | `Backtick` | Re-sort every window to its workspace (wake recovery) | | `/` | Show the cheatsheet | | `Esc` | Exit launch mode | ## Service mode — ⌥⇧; (prowl) | Keys | Action | |---|---| | `R` | Flatten the workspace tree | | `F` | Toggle floating ↔ tiling | | `Backspace` | Close all windows except current | | `⌥⇧H/J/K/L` | Join with the neighbour | | `↑` / `↓` | Volume up / down | | `⇧↓` | Mute | | `Esc` | Reload config and exit | ## Terminal — zellij (hearth) | Keys | Action | |---|---| | `Super P` | New pane (inherits cwd; hops to the main checkout inside a worktree) | | `Super ⇧P` | New pane, stay here (inherits cwd, no worktree hop) | | `Super T` | New tab at `$HOME` (born named `~`) | | `Super ⇧T` | New tab at the focused pane's directory (same worktree hop as `Super P`) | | `Super F` | Toggle the focused pane fullscreen (zoom to fill the tab, again to restore) | | `Super R` | Reload the terminal stack — quit/reopen Ghostty and restart zellij with the same tabs, panes and cwds; live Claude panes resume (also `zreload` from a shell) | | `Super Y` | yazi peek (floating previews; `Enter` on a dir opens a new tab there) | | `Super ⇧Y` | yazi jump (browse, then shell in that dir) | | `Super L` | Open Links picker (every URL from focused pane's transcript/scrollback) | | `Ctrl Tab` / `Ctrl ⇧Tab` | Tab history back / forward (most-recently-used, browser-style) | | `Alt [` / `Alt ]` | Cycle swap layouts (grid → spiral → columns) — works locked too | | `Super C` | Spawn an isolated agent (own worktree) in a new pane — your [`agents.default`](/reference/options/) client: Claude Code via its own `--worktree`, Codex or Opencode via `wt new` | | `Super ⇧C` | The same agent, **in place of** the focused pane instead of beside it — the replaced pane is suspended, not killed, and comes back when the agent quits | | `Ctrl ⌥⇧C` | Spawn a resident agent (this checkout) | **Locked by default.** zellij boots in **Locked** input mode, so its single-key submode leaders (pane, tab, resize) stay inert until you press `Ctrl g` — a stray keystroke can't drop you into a submode. The `Super`-prefixed launchers above work regardless of the lock, and so do `Alt [` / `Alt ]` — picking a layout is a workspace act, not a submode. The rest of zellij's `Alt` row stays inert while locked on purpose: `Alt h/j/k/l` and `Alt`-arrows are word motions the shell and your editor want. Flip it with [`nebelhaus.hearth.zellijStartLocked`](/reference/options/#nebelhaushearthzellijstartlocked). **Clickable links** work across two modifiers. `⌥`-click a file path (or a visible URL / bare domain) in the terminal to open it — a path opens a new tab `cd`'d there, a link opens in the browser. `⌘`-click opens any web link, including **embedded hyperlinks** whose visible text isn't the URL (e.g. Claude Code's `/tui` session and PR links) — those only respond to `⌘`-click, since the URL is hidden in the terminal escape sequence rather than shown on screen. ## Pounce — ⌘Space | Keys | Action | |---|---| | type | Fuzzy-search | | `↑` / `↓` | Move selection | | `Return` | Default action | | `⇧Return` | Insert a newline (the query field is multi-line) | | `⌘Return` | Modifier action (e.g. Reveal in Finder) | | `⌥Return` / `⌃Return` | Alternate actions (when shown) | | `⌥Return` *(Find Files)* | Copy the path | | `Tab` | Cycle sections / emoji categories | | `Esc` | Dismiss | ## Ghostty note Ghostty deliberately **unbinds** `⌘T`, `⌘P`, `⌘⇧P`, `⌘Y`, `⌘⇧Y`, `⌘⇧T`, `⌘F`, `⌘R`, `⌘C` and `⌘⇧C` so zellij owns them — the same keys work whether or not you're multiplexed. (`⌘⇧C` is unbound pre-emptively: Ghostty claims nothing there today, and the unbind keeps it that way if a future release does.) `Ctrl-Tab` is forwarded to zellij via the kitty keyboard protocol. `⌘D` and `⌘⇧D` are unbound too, but nothing takes them over: they do **nothing**. Ghostty's defaults there are its own `new_split:right` / `new_split:down`, which nest a second terminal surface *inside* the one zellij is driving — invisible to the multiplexer, so its layout, status bar and pane keys would apply to only half the window. Since `⌘D` is muscle memory from other terminals, it's an easy accident; use `Super P` for a pane instead. --- # Pounce config & CLI Source: https://nebelhaus.com/reference/pounce/ Every config key, CLI flag, and file path for the Pounce command palette. Reference for configuring and driving [Pounce](/guides/pounce/). For writing your own commands, see [Writing pounce commands](/guides/pounce-commands/). ## Config file Pounce reads `~/.config/pounce/config.json`. Every key is optional; the file is re-read each time the palette opens (no daemon restart needed). ```jsonc { "theme": "nebelung", // "nebelung" (default), "mocha", or a themes/ file "themeLight": "nebelung-latte", // used when macOS is in Light Mode "themeDark": "nebelung", // used when macOS is in Dark Mode "windowMode": "default", // "default" (720px) or "compact" (600px, tighter) "hotkey": { "enabled": true, // register the global hotkey in-process "key": "space", // "space", "return", "tab", "escape", "a"–"z", "0"–"9" "modifiers": ["cmd"] // any of "cmd", "shift", "opt", "ctrl" }, "clipboard": { "enabled": true, // watch the pasteboard "maxEntries": 200, // history size "blacklistBundleIds": ["com.apple.Passwords"], // never record from these "autoPaste": false // synthesize ⌘V into the prior app (needs Accessibility) }, "quickAnswers": { "currency": true // fetch ECB rates so "100 usd in eur" answers inline }, "updates": { "check": true // nudge (never install) when a new release is out }, "fileSearch": { "enabled": true, // the Find Files mode "homeOnly": true, // scope to ~ instead of the whole index "maxResults": 60 // rows kept per query }, "apps": { "demoteBundleIds": [], // sink these apps below everything else "hideBundleIds": [] // drop these apps from the list entirely }, "windows": { "enabled": false, // the MRU window switcher (needs Accessibility) "key": "tab", // hold the modifiers, tap this to walk windows "modifiers": ["cmd"] }, "items": { // per-item enable / alias / hotkey — see below "cmd:emoji": { "alias": "emo", "hotkey": "opt+space e" } } } ``` | Key | Values | Default | |---|---|---| | `theme` | `"nebelung"` \| `"mocha"` \| a `themes/` file name | `"nebelung"` | | `themeLight` | same values; applies in macOS Light Mode | `"nebelung-latte"` | | `themeDark` | same values; applies in macOS Dark Mode | `"nebelung"` | | `windowMode` | `"default"` \| `"compact"` | `"default"` | | `hotkey.enabled` | `true` \| `false` | `true` | | `hotkey.key` | key name | `"space"` | | `hotkey.modifiers` | array of `cmd`/`shift`/`opt`/`ctrl` | `["cmd"]` | | `clipboard.enabled` | `true` \| `false` | `true` | | `clipboard.maxEntries` | number | `200` | | `clipboard.blacklistBundleIds` | array of bundle ids | `["com.apple.Passwords"]` | | `clipboard.autoPaste` | `true` \| `false` | `false` | | `quickAnswers.currency` | `true` \| `false` | `true` | | `updates.check` | `true` \| `false` | `true` | | `fileSearch.enabled` | `true` \| `false` | `true` | | `fileSearch.homeOnly` | `true` \| `false` | `true` | | `fileSearch.maxResults` | number | `60` | | `apps.demoteBundleIds` | array of bundle ids | a built-in list of background/helper apps | | `apps.hideBundleIds` | array of bundle ids | `[]` | | `windows.enabled` | `true` \| `false` | `false` | | `windows.key` | key name | `"tab"` | | `windows.modifiers` | array of `cmd`/`shift`/`opt`/`ctrl` | `["cmd"]` | | `items` | map of item key → `{ enabled, alias, hotkey }` | `{}` | `themeLight` / `themeDark` are resolved per open (like everything else here), so flipping macOS appearance shows on the next summon. Either one falls back to `theme`, and `theme` alone pins one palette for both modes. Setting `windows.enabled` turns on the MRU [window switcher](/guides/pounce/#a-window-switcher-not-an-app-switcher-opt-in); it needs the Accessibility grant to install its event tap, and without the grant stock ⌘Tab keeps working. `quickAnswers.currency` and `updates.check` are the only two things in Pounce that touch the network. The first fetches the ECB daily reference rates from `api.frankfurter.app` (at most every 12 hours, cached to `~/.local/share/pounce/currency-rates.json`). The second asks GitHub once an hour whether there is a newer release. Set both to `false` and Pounce makes no network requests at all. `updates.check` only ever *tells* you — it never installs anything. While a release is pending, the **Update Pounce** row is renamed with the new version and pinned to the palette's first row, and a notification repeats at most once a day. Press `⌘⏎` on that row to skip the version: the pin and the notification stop until the next release, though searching for the row still shows what's waiting. The wording follows how you installed Pounce, because not every install can update itself in place: | Install | What the nudge says | |---|---| | Homebrew | Return runs `brew upgrade pounce` and restarts the service | | Dragged to `/Applications` | Return downloads the new release and swaps the app in place | | The nebelhaus rice | Run [`haus update`](/reference/haus/) — Pounce comes down with the rest of the rice | | Your own flake | Update your `pounce` input | The last two update through the Nix store rather than in place, so Pounce deliberately won't try to overwrite itself there — the next rebuild would revert it anyway. Setting `hotkey.enabled` to `false` frees the hotkey so an external launcher (skhd, AeroSpace) can bind a key to `pounce-palette` instead. Any `theme` value that isn't a built-in resolves to `~/.config/pounce/themes/.json` — a flat catppuccin-style `name → "#hex"` map ([nebelung's](https://github.com/nebelhaus/nebelung) `palette/*.hex.json` files verbatim), re-read on each open like the config itself. That's how the rice's `theme.flavor` / `theme.contrast` reach Pounce without a rebuild, and it works the same on a Homebrew install: ```sh mkdir -p ~/.config/pounce/themes curl -fsSLo ~/.config/pounce/themes/nebelung-latte.json \ https://raw.githubusercontent.com/nebelhaus/nebelung/main/palette/nebelung-latte.hex.json # config.json: "theme": "nebelung-latte" ``` An unknown name or malformed file falls back to the built-in nebelung palette. ## Per-item settings (`items`) One map covers what you'd otherwise want three keys for — hide a row, give it a search shorthand, give it a global key. Each entry is keyed by an **item key**: | Item key | Addresses | |---|---| | `cmd:` | a command script, by filename without `.sh` | | `app:/Applications/Foo.app` | an application, by path | | `mode:` | a built-in window — `launcher`, `clipboard`, `emoji`, `screenshots`, `camera`, `filesearch` | ```jsonc { "items": { "cmd:emoji": { "alias": "emo", "hotkey": "opt+e" }, "cmd:brew-services": { "enabled": false }, "app:/Applications/Ghostty.app": { "alias": "term", "hotkey": "opt+t" }, "mode:clipboard": { "hotkey": "cmd+shift+v" } } } ``` - **`enabled: false`** drops the row from the launcher. It does *not* disarm a hotkey you bound to it — keeping an item off the list but on a key is a legitimate setup. - **`alias`** is a search shorthand, matched at a bonus over the item's real name so it wins over whatever app fuzzy-matches the same letters. - **`hotkey`** runs the item directly, skipping the palette. The last segment is the key, the rest modifiers (`cmd`/`shift`/`opt`/`ctrl`); the `{"key": …, "modifiers": …}` object form works here too. ### Leader sequences Add a **space** for a two-step key: whitespace separates steps, `+` separates modifiers, so `"opt+space e"` is ⌥Space then E — the notation Emacs and VS Code use. Sequences sharing a leader share it (⌥Space registers once and owns a map of next keys), and a sequence can run longer than two: `"opt+space g s"`. The point on a tiling setup: a leader opens a namespace that can't collide with the ⌥/⌘ chords AeroSpace already owns, and it needs **no Accessibility grant** — pressing the leader grabs its next-step keys as ordinary global hotkeys for ~2s and releases them the instant one fires, so there's no event tap and no TCC prompt. Escape cancels; hesitate ~0.45s and a which-key overlay lists the next keys. `pounce doctor` reports every binding it actually armed, so a typo or a key some other app holds is visible at startup, not on the day you press it. ### Driving pounce from another binder If a tool already owns your keystrokes — AeroSpace binding modes, skhd, Shortcuts — let it do the chord and have pounce do the action: ```sh pounce run cmd:emoji pounce run mode:clipboard ``` Same target grammar as `items`, dispatched through the identical path a native binding takes. It exits non-zero with a reason on a malformed target, so a typo fails loudly instead of arming a key that does nothing. ## CLI ```sh pounce --launcher # apps + commands palette (the default mode) pounce --max-empty 7 # rows to show before you type pounce -p "Pick:" # generic picker; reads lines from stdin pounce -i "sf.symbol.name" # icon for the picker pounce -p "Search:" --chain # picker whose free-text Enter feeds another pounce step pounce run cmd:emoji # run one item by its key (for external binders) # built-in windows — items, not flags (`pounce run `) pounce run mode:clipboard # clipboard history pounce run mode:emoji # emoji picker pounce run mode:screenshots # screenshot browser pounce run mode:camera # live camera preview pounce run mode:filesearch # file/folder search (Spotlight index) pounce run cmd:hush # any command, by script name without .sh pounce run app:/Applications/Ghostty.app pounce --cheatsheet [path] # cheatsheet overlay pounce --transform 'tr a-z A-Z' # rewrite the selected text through a shell filter # housekeeping pounce doctor # diagnose a dead/slow hotkey (see Troubleshooting) pounce --help # every flag, from the binary itself pounce --version pounce --daemon # run the resident daemon (launchd uses this) pounce --copy-file # copy a file (contents) to the clipboard pounce --request-accessibility # prompt for the Accessibility grant pounce --check-accessibility # exit 0 / prints true when granted pounce --request-bluetooth # prompt for the Bluetooth grant (v0.4.4+) pounce --check-bluetooth # exit 0 / prints true when granted ``` | Flag | Purpose | |---|---| | `-p`, `--placeholder` | Prompt text for the search field | | `-i`, `--icon` | SF Symbol icon for the picker | | `--chain` | Mark a free-text Enter as feeding another `pounce` step — holds the window with the loading skeleton instead of fading. See [two-step commands](/guides/pounce-commands/#two-step-commands-submenus) | | `run ` | Run one item by the key `items` uses (`cmd:emoji`, `mode:clipboard`, `app:/…`), for binders that own the keystroke | | `--launcher` | Apps + commands mode | | `--max-empty N` | Rows shown before any query is typed | | `--cheatsheet [path]` | Overlay a cheatsheet (JSON) | | `--transform ` | Pipe the current selection through a shell filter and paste the result back (needs Accessibility) — how **Capitalize** / **Lowercase** work | | `run ` | Run one item: `mode:clipboard` \| `mode:emoji` \| `mode:screenshots` \| `mode:camera` \| `mode:filesearch` \| `mode:launcher`, `cmd:`, `app:`. The built-in windows had a flag each until 2026-07-30 (`--clipboard` and friends); they're items now, so one name works as a palette row, a hotkey target and a CLI argument. Needs the daemon | | `--version` | Print the version | | `--request-accessibility` / `--check-accessibility` | Manage the Accessibility (TCC) grant | | `--request-bluetooth` / `--check-bluetooth` | Manage the Bluetooth (TCC) grant — the bluetooth plugin calls this for you | ## File paths | Path | What | |---|---| | `~/.config/pounce/config.json` | Configuration | | `~/.config/pounce/commands/` | Your commands (highest precedence) | | `~/.config/pounce/cheatsheet.json` | Optional cheatsheet content | | `~/.local/share/pounce/frecency.json` | Usage history for ranking | | `~/.local/share/pounce/pounce.sock` | Daemon control socket | ## Environment variables Set by packagers (the rice), rarely by hand: | Variable | Meaning | |---|---| | `POUNCE_BUILTIN_DIR` | Directory of built-in commands | | `POUNCE_EXTRA_COMMAND_DIRS` | Colon-separated extra command dirs (Nix layers) | | `POUNCE_COMMAND_PATH` | Colon-separated ad-hoc command dirs | ## Homebrew binaries Installing via `brew install pounce` puts these on your `PATH`: | Binary | What | |---|---| | `pounce` | The app / daemon | | `pounce-palette` | The launcher wrapper (bind a hotkey to this) | | `pounce-` | A wrapper per built-in command (e.g. `pounce-clipboard`) | Inside nebelhaus, set the palette up via [`nebelhaus.pounce`](/reference/options/#nebelhauspounce) instead — the module handles the daemon, hotkey, and permission survival for you. --- # The nebelung palette Source: https://nebelhaus.com/reference/palette/ Every colour in the silver-mist palette, with hex values, plus how to use nebelung in your own tools. export const NOTES = { mauve: 'the family default' }; {/* The chip paints itself with the live CSS variable and the label prints the value parsed out of palette.css, so neither can drift from the stylesheet. Nothing on this page states a hex. */} export const Swatch = ({ name }) => (
{hex(name)} {NOTES[name] ? `${name} — ${NOTES[name]}` : name}
); **nebelung** is a silver-mist Catppuccin variant — the blue pulled out of the neutral ramp and the accents calmed (chroma ×0.9), keeping perceptual lightness identical. The result is a fog-grey scheme named for a cat breed the colour of high fog. Grey is the point. The source of truth is `palette/nebelung.hex.json` in the [nebelung repo](https://github.com/nebelhaus/nebelung). ## The four variants Those two rules never mention "dark", so they work in either polarity: applied to Catppuccin **Latte** instead of Mocha they give the same theme as a light one. Crossed with a contrast axis, that's four palettes — picked in the rice with [`theme.flavor` and `theme.contrast`](/guides/theming/#light-mode): | Variant | Source | Rendered under | text on base | |---|---|---|---| | `nebelung` | Mocha | *(the tree root)* | 11.3:1 | | `nebelung-high-contrast` | Mocha | `high-contrast/` | 19.9:1 | | `nebelung-latte` | Latte | `latte/` | 7.0:1 | | `nebelung-latte-high-contrast` | Latte | `latte-high-contrast/` | 9.9:1 | Every variant keeps its flavor's accents unchanged — contrast is a property of the **neutrals** only, so a high-contrast rice is the same theme read more easily rather than a different one. Each ratio is asserted in nebelung's CI, and there's a test that all twelve ramp steps stay distinct, because a boost pushed too far clamps two of them into the same colour and silently costs a level of hierarchy. Each variant also renders as its own Catppuccin flavor, so its files are named after it — Ghostty's light theme is `latte/ghostty/themes/catppuccin-latte.conf`. The swatches below are the default variant; view any of them in the [interactive preview](https://htmlpreview.github.io/?https://github.com/nebelhaus/nebelung/blob/main/preview/nebelung-latte.html). ## The neutral ramp Twelve steps from near-black to soft white — the bones of the theme.
{RAMP.map((name) => )}
## The accents Fourteen calmed accents. **mauve** is the family default; set another with [`nebelhaus.theme.accent`](/reference/options/#nebelhaustheme).
{ACCENTS.map((name) => )}
## Using it in your own tools nebelung ships a rendered theme for **50+ tools**, generated from the palette with [Whiskers](https://github.com/catppuccin/whiskers). A sampling: | Tool | Tool | Tool | Tool | |---|---|---|---| | Ghostty | Kitty | Alacritty | Starship | | zellij | tmux | btop | bat | | delta | fzf | lsd | yazi | | lazygit | glow | helix | Zen browser | | Slack | Obsidian | VS Code / Cursor | zsh-syntax-highlighting | Two ways to consume them: - **Copy a rendered file.** Build the flake's output tree and grab the file for your tool — e.g. Ghostty's theme, bat's `.tmTheme`, lazygit's per-accent `.yml`. Each tool's README section shows where its config expects the file. - **Consume the flake** from your own Nix config: ```nix inputs.nebelung.url = "github:nebelhaus/nebelung"; # rendered themes: # ${nebelung.packages.${system}.default}//... # raw palette (name → "#hex") for configs you generate yourself: # nebelung.palette ``` **Per-accent tools** lazygit, yazi, and the Zen browser render **all 14 accents** as separate files; consumers pick one (the rice uses `mauve`). The single-file tools ship one theme with the default accent baked in. ## Tuning the palette The palette is computed in [OKLCH](https://oklch.com/) space, so you can rewrite hue and chroma while preserving perceptual lightness. Three knobs at the top of `scripts/generate-palette.mjs` control the look: | Knob | Meaning | Default | |---|---|---| | `neutralHue` | Hue (degrees) of the grey tint — 70° is a faint warm graphite | `70` | | `neutralChroma` | Strength of that tint; `0` is pure neutral grey | `0` | | `accentChromaScale` | How much accents are calmed (`0.9` = 10% less saturated) | `0.9` | Then regenerate and re-render: ```sh ./build.sh # regenerate palette + render every tool → dist/ ./build.sh --no-gen # render only (palette unchanged) ``` This is a change in the nebelung repo, not your host file — see the nebelung [README](https://github.com/nebelhaus/nebelung) for the full workflow. --- # The haus CLI Source: https://nebelhaus.com/reference/haus/ The end-user haus command — rebuild, update, rollback, and diagnose your machine. `haus` is the command that drives your machine after install. It wraps the Nix and `darwin-rebuild` invocations you'd otherwise type by hand, with safe defaults (it always builds before switching). It lands on your `PATH` after the first `switch`. For the day-to-day workflow, see [Keeping in sync](/guides/staying-in-sync/). ## Commands | Command | What it does | |---|---| | `haus rebuild` | Build, then `darwin-rebuild switch`. Your everyday apply. A failed build never touches the running system. | | `haus update` | Update the `nebelhaus` pin in `~/.config/nix/flake.lock`, then rebuild — pulls new rice versions. | | `haus rollback [N]` | Atomically return to the previous generation — or to generation `N`. | | `haus generations` | List the generations you can roll back to. | | `haus status` | Show the current generation and how stale the pinned rice is. | | `haus edit` | Open your host file (`~/.config/nix/hosts//default.nix`) in `$EDITOR`. | | `haus doctor` | Health check: Determinate Nix, Xcode CLT, the GUI login agents, Homebrew cask drift (casks installed that no rebuild will manage), and whether an [agent](/guides/ai-agent/) can change this machine. | | `haus btm` | On macOS 26 Tahoe+, check whether Background Task Management is blocking the nix login agents, and print the one-time fix. A no-op on earlier macOS. See [Troubleshooting](/reference/troubleshooting/#after-a-macos-upgrade-all-my-agents-are-dead-macos-26-tahoe). | ### One thing `haus rebuild` refuses If your host file sets `system.defaults.universalaccess.*`, `haus rebuild` refuses to run from an AI agent session that lacks Full Disk Access — that write would abort activation partway and skip every background service the rice installs. Run the same command yourself instead. See [Changing your Mac with an agent](/guides/ai-agent/#the-one-rebuild-it-will-refuse). ## Typical sessions **Change a setting and apply it:** ```sh haus edit # tweak your host file haus rebuild ``` **Pull the latest rice:** ```sh haus update # bumps the pin and rebuilds ``` **Recover from a bad change:** ```sh haus rollback # back to the previous generation haus doctor # if something still looks off ``` ## The other CLIs `haus` is the only command an end user needs. The rice and workshop ship three more, each for a different job — see [the CLIs at a glance](/start/the-family/#the-clis-at-a-glance) for the full map: - **[`wt`](/guides/claude-agents/)** — agent worktrees (Claude Code, Codex, OpenCode) for any repo. Also on your `PATH` (it ships in the rice), and useful to anyone who runs Claude Code, contributor or not. - **[`bench`](/internals/contributing/)** — the contributor CLI in the workshop checkout: `try`, `ship`, `release` for moving changes between the family's repos. - **`zscratch`** — feel-test a zellij edit without a rebuild (for rice contributors); ships in the rice. --- # Troubleshooting Source: https://nebelhaus.com/reference/troubleshooting/ The handful of things that go sideways on macOS — a wedged agent, a missing grant, a stubborn cask — and the exact command to unstick each one. Most nebelhaus trouble is one of a few known macOS quirks with a known fix. Work top-down; `haus doctor` is a good first stop for any of them. ```sh haus doctor # Nix, the Xcode CLT, the GUI agents, and cask drift, in one shot ``` ## ⌘Space / Pounce does nothing The launcher is a launchd **user agent**. If it isn't answering: ```sh # Is it running? launchctl list | grep pounce # Bounce it (the clean recover for any wedged user agent): launchctl bootout gui/$(id -u)/org.nixos.pounce 2>/dev/null launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.nixos.pounce.plist ``` If it opens but **clipboard/emoji paste** don't work, it's missing the Accessibility grant: ```sh pounce --request-accessibility # approve the system dialog pounce --check-accessibility # prints true when granted ``` Set `nebelhaus.pounce.signingIdentity` so this grant survives rebuilds — see [Pounce config](/reference/pounce/). If ⌘Space still opens **Spotlight**, log out and back in once so the symbolic-hotkey reassignment takes. **Palette opens, but slowly?** An external hotkey tool — skhd, AeroSpace, Raycast — bound to the same key grabs it with an event tap *ahead* of pounce, so every summon spawns a fresh client instead of hitting the daemon's fast in-process path. Registration still reports success and the palette still opens, which is what makes this one so hard to spot. One command names the culprit: ```sh pounce doctor ``` It combines the daemon's live hotkey self-report with outside-in probes — a macOS system-shortcut clash, running external hotkey daemons, and their configs scanned for a binding on pounce's key — and tells you what to unbind. ## Windows won't tile / AeroSpace isn't running This is almost always the **cold-boot GUI race**: an agent launched before the desktop session was ready parked itself (exit 78) instead of starting. The rice guards against it, but if you land in this state, bounce the agent: ```sh launchctl bootout gui/$(id -u)/org.nixos.aerospace 2>/dev/null launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.nixos.aerospace.plist ``` Other quick fixes: - **Windows scrambled after sleep?** They self-heal after a couple seconds; force it by tapping ⇪, then `Backtick`. - **A binding you changed didn't take?** A rebuild rewrites the config but the live daemon caches the old one — normally auto-reloaded, but you can force it: `aerospace reload-config`. - **A window landed on the wrong workspace?** Tap ⇪, then `Backtick` to re-sort everything to its roster home. ## The bar (SketchyBar) is blank or missing ```sh launchctl bootout gui/$(id -u)/org.nixos.sketchybar 2>/dev/null launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.nixos.sketchybar.plist # or, if it's running but stale: sketchybar --reload ``` If you'd rather not run a custom bar at all, set `nebelhaus.sill.enable = false` and the native macOS menu bar comes back. ## After a macOS upgrade, *all* my agents are dead (macOS 26 Tahoe+) If the bar, tiling, and ⌘Space all come up dead at once right after upgrading to **macOS 26 Tahoe or later**, the culprit is usually **Background Task Management (BTM)**. Tahoe gates login items whose executable isn't Apple-signed, and every nix agent launches through `/bin/sh -c "…"` — which BTM files under "unidentified developer" and can silently refuse to start. The agents register fine; they just never run. `haus doctor` flags this on Tahoe+; confirm and get the fix with: ```sh haus btm # no-op before Tahoe; on Tahoe+ it reads the BTM store and instructs ``` There's **no declarative fix** — the toggle lives in macOS's BTM store, which has no CLI to set it (`sfltool` can only dump it). So it's a one-time manual step: 1. **System Settings → General → Login Items & Extensions** 2. Scroll to **"Allow in the Background"** 3. Find the entries named **`sh`** — subtitle *"Item from unidentified developer"* 4. Toggle them **on**, then reboot *(already on but still blocked? flip off then on to force a database write)* Inspect the store by hand any time with: ```sh sudo sfltool dumpbtm | grep -B2 -A8 -iE "nixos|darwin-store" # look for "disallowed" ``` ## Touch ID for sudo beachballs (inside tmux/zellij) If the Touch ID prompt hangs when you `sudo` inside a multiplexer, the `pam_reattach` shim isn't loading. It ships with the **collar** room and is on by default; confirm the order in `/etc/pam.d/sudo_local` (reattach must come *before* the Touch ID line). Outside a multiplexer, Touch ID should just work; cancel the prompt to fall back to your password. ## "Refusing to load cask … from an untrusted tap" Third-party taps fail Homebrew's trust check under sudo-driven activation. The rice disables that requirement globally (`HOMEBREW_NO_REQUIRE_TAP_TRUST=1` in `/etc/homebrew/brew.env`). If you see this, that file didn't land — re-run `haus rebuild`, or set the variable in your shell for the one-off. ## An app I removed from my config is still installed By design. `nebelhaus.homebrew.cleanup` defaults to `"none"`, so nothing you installed is ever auto-deleted, and **`haus rollback` doesn't rewind Homebrew apps** (they're not in Nix generations). Remove one by hand: ```sh brew uninstall --zap ``` Want a fully declarative machine where undeclared casks get removed on rebuild? Set `nebelhaus.homebrew.cleanup = "zap";` — see [Making it yours](/guides/making-it-yours/#homebrew-behaviour). ## A rebuild broke something — get back fast ```sh haus rollback # atomically return to the previous generation haus generations # see what you can roll back to ``` `haus rebuild` always builds *before* switching, so a config with an error can't activate — you just see the build fail. For macOS **settings** (not packages), the APFS/Time-Machine snapshot the installer took is the coarser rewind. Want to go further than one generation — disable a room, or remove nebelhaus entirely? [Leaving nebelhaus](/guides/leaving/) walks each exit, smallest first. ## The installer refuses to run — "expects Determinate Nix" nebelhaus is built on [Determinate Nix](https://docs.determinate.systems/) and won't install on top of a stock/single-user Nix rather than risk breaking it. If you have an existing Nix you don't need, uninstall it and re-run the one-liner; if you want to keep it, migrating to Determinate is the supported path. Fresh Macs need none of this — the bootstrap installs Determinate for you. ## Still stuck? - [Keybindings cheatsheet](/reference/keybindings/) — or tap `⇪` then `/` for the live, roster-aware version. - [nebelhaus.* options](/reference/options/) — every knob and its default. - [Open an issue](https://github.com/nebelhaus/nebelhaus/issues) with the output of `haus doctor`. --- # How the flakes fit together Source: https://nebelhaus.com/internals/flakes/ The consumer config, the mkNebelhaus builder, and why pinned flake inputs mean nothing changes until you say so. nebelhaus is built on [Nix flakes](https://nixos.org/). You don't need to understand Nix deeply to use the rice, but knowing how the pieces fit makes the `update` / `rebuild` model click — and it's essential if you want to [contribute](/internals/contributing/). ## Your config is a thin consumer The installer scaffolds `~/.config/nix` as a **thin consumer** of the rice. Its `flake.nix` imports nebelhaus as an input and calls the `mkNebelhaus` builder: ```nix { inputs.nebelhaus.url = "github:nebelhaus/nebelhaus"; outputs = { nebelhaus, ... }: { darwinConfigurations. = nebelhaus.mkNebelhaus { username = ""; hostname = ""; host = ./hosts/; }; }; } ``` All your personal choices live in `hosts//default.nix` — your [options](/reference/options/), your app roster, your casks. The rice stays upstream; you never edit it directly. ## The mkNebelhaus builder `mkNebelhaus` is the rice's entry point. Given a username, hostname, and host file, it returns a full `darwin-darwinSystem` with every room wired up: ```nix nebelhaus.mkNebelhaus { username = "ada"; hostname = "fog"; host = ./hosts/fog; # your identity + choices system = "aarch64-darwin"; # Apple Silicon only extraModules = [ ]; # extra nix-darwin modules, optional } ``` It composes the rooms — `den`, `theme`, `hearth`, `prowl`, `sill`, `collar`, `pounce`, `trill`, `perch`, `hush`, `secrets` — and layers in home-manager, the [nebelung](/reference/palette/) theme, and the [pounce](/guides/pounce/) app. Most rooms are also exported individually under `darwinModules` (`den`, `hearth`, `prowl`, `sill`, `collar`, `pounce`, `hush`, `secrets`, and `default` for all of them) if you'd rather cherry-pick; `theme`, `trill`, and `perch` ride along only with the full house. ## The inputs The rice flake pulls together: - `nixpkgs` (unstable) — the package set - `nix-darwin` — macOS system modules - `home-manager` — your user environment - `catppuccin` — the theme framework - `nebelung` — the silver-mist palette (a family input) - `pounce` — the launcher app (a family input) - `trill` — the Messages client (a family input; tracks trill *releases*, not `main`) - `perch` — the notch file shelf (a family input; tracks perch *releases*, not `main`) - `nix-index-database` — for `nix-index` + `comma` ## Why pins mean "nothing changes until you say so" Each input is recorded in `flake.lock` as an **exact commit hash**, not "latest". That's what makes a rebuild reproducible — the same lock produces the same system on any machine, any day. The flip side, and the thing that surprises everyone: pushing a change to nebelung or the rice changes **nothing** on your machine until your `flake.lock` is bumped to point at the new commit. A colour change therefore ripples like this across the family: ![one colour change rippling down the chain: nebelung → pounce → nebelhaus → ~/.config/nix → your Mac, each lock pinning the exact commit of the one before](/media/ripple.webp?v=2) **haus update does the ripple for you** As an end user you never walk this by hand — `haus update` bumps your pin to the latest nebelhaus (which already has its own inputs pinned) and rebuilds. Family *contributors* use the workshop's `bench ship` to ripple a change all the way down the chain; see [Contributing](/internals/contributing/). ## Cold-boot safety One subtlety worth knowing if you ever debug a login issue: Determinate Nix lives on a separate APFS volume, and macOS sometimes tries to launch the rice's GUI agents (AeroSpace, SketchyBar, Pounce) before that volume mounts or before the GUI event manager is ready. The rice wraps each agent in a small `gui-wait` shim that polls for Dock/Finder/SystemUIServer readiness before exec'ing — which is why your bar and tiling reliably come back after a reboot. --- # Contributing & worktrees Source: https://nebelhaus.com/internals/contributing/ How the family repos fit together, the workshop bench CLI, and the hack → test → ship → release flow. Want to hack on nebelhaus itself — the rice, the launcher, or the palette? This page covers how the family is developed. (To just *use* nebelhaus, you never need any of this — see [Keeping in sync](/guides/staying-in-sync/).) ## The workshop The [workshop repo](https://github.com/nebelhaus/workshop) is a parent directory that holds every family repo checked out side by side, plus a `bench` dev CLI (a different tool from the [end-user `haus`](/reference/haus/)). Clone it and pull the family: ```sh git clone https://github.com/nebelhaus/workshop.git cd workshop ./bench clone ``` You end up with `nebelung/`, `pounce/`, `nebelhaus/`, and friends as independent git repos, with the `bench` script sitting above them. ## Where a change goes Each change belongs to exactly one repo. Start in the right one: | Change | Repo | |---|---| | colours / palette / how a tool is themed | `nebelung` | | the launcher app or a generic command | `pounce` | | the Messages client (trill) | `trill` | | the notch file shelf (perch) | `perch` | | macOS defaults, tiling, bar, shell, Touch ID | `nebelhaus` | | your own machine's apps / identity / secrets | `~/.config/nix` | ## The dev loop: try without pushing The important trick is that you never have to push to *see* a change. The workshop `bench try` builds your real machine config against the **local checkouts** — uncommitted edits and all — using Nix `--override-input`: ```sh # edit anything in nebelung/, pounce/, nebelhaus/… ./bench try # does it build? (nothing pushed, nothing activated) ./bench try switch # run it on this Mac (still nothing pushed) ``` When you're happy, commit in the repos you touched and let `bench ship` ripple the change downstream — pushing each repo and bumping every `flake.lock` in dependency order, so a colour change in nebelung reaches your Mac without you hand-walking the [pin chain](/internals/flakes/#why-pins-mean-nothing-changes-until-you-say-so): ```sh ./bench ship ``` `./bench status` shows every repo's git state and every stale lock edge at a glance. ## Parallel agents & worktrees nebelhaus is built to be hacked on by several [Claude Code](https://claude.com/claude-code) agents at once. Each agent runs in its own **git worktree** — an isolated checkout on a `worktree-` branch, living outside the repo — so agents never yank the branch out from under each other. That whole mechanism (spawning with `Super c`, never losing work when a pane closes, resuming a parked session, the statusline HUD) is the rice's [`wt` tool](/guides/claude-agents/), and it works for any repo. Two things matter specifically when the repo is a *family* one: - **`bench try` is worktree-aware.** Run it from inside a worktree and that repo's flake override points at *your branch* instead of the main checkout — so an agent can prove its branch builds before anyone merges it. (`bench try switch` refuses from a worktree: only the main checkouts get activated.) - **Land work through a PR — never a push or local merge into `main`.** Parallel agents pushing straight to `main` clobber each other; a PR is conflict-detected and atomic. Push the branch, open a PR with a **What / Why / Verify / Watch-out** body, and merge it from GitHub. `bench status` lists every agent worktree and unmerged `worktree-*` branch so nothing rots silently. Working from the workshop on a task that belongs to a *sub*-repo? Resolve the workshop's main checkout from Git, then make the child worktree — this works no matter where you cloned the workshop: ```sh workshop_root="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")" cd "$(wt child "$workshop_root/nebelung")" # replace nebelung with the repo you need ``` That registers the child so its PR shows in the statusline, which a raw `git worktree add` would not. ## Feel-testing the whole PR queue (try-batch) Activating a Mac is **serial** — one `darwin-rebuild switch` is one machine state — so with a stack of PRs waiting you can only feel-test one at a time. The tempting shortcut is to merge them all to `main` first, then rebuild and tick them off — but now unverified code is on `main` before you've felt it. `bench try-batch` inverts that. For each repo it builds a throwaway integration tree (the committed `main` plus every open PR merged in), overrides the flake at those trees, and builds — or activates — the whole queue in **one** rebuild, `main` untouched: ```sh ./bench try-batch # build every open PR together; prints a tick-off checklist ./bench try-batch switch # …and activate the combined tree on this Mac ``` Verify each PR (its body carries the Verify steps), then merge only the ones that pass and leave the rest open. This is test-then-merge, not merge-then-test. ## Feel-testing a zellij edit (zscratch) One rice change escapes the ripple entirely. A zellij edit — a keybind, a theme colour, a freshly-built plugin `.wasm` — doesn't need `bench try switch` at all: that restarts the `main` zellij session and nukes every open tab. The rice ships **`zscratch`** for exactly this. It renders your candidate over a copy of your live `~/.config/zellij` into a temp config dir and boots a throwaway session in its **own Ghostty window**, so your working multiplexer is untouched: ```sh zscratch --config config.kdl # try a candidate config.kdl zscratch --layout my-layout.kdl # …or a layout zscratch --theme nebelung.kdl # …or a theme zscratch --plugin tab-bar=tab-bar.wasm # …or swap a freshly-built plugin zscratch clean # kill the scratch session + temp dir ``` Feel it there; the real `bench try switch` happens once, at the end, already knowing it works. After that activation, press `Super R` (or run `zreload`) to gracefully quit/reopen Ghostty and load the installed zellij config and plugin wasm through a fresh server without rebuilding the workspace by hand. The destructive half runs in a launchd agent outside the terminal, so it survives both processes going away. Tabs, panes and working directories return; live Claude Code panes resume their exact conversations. Pane identity comes from a generation-scoped manifest rather than zellij's foreground-command label (which can temporarily be an LSP or tool); an unclassified command pane aborts before the launchd handoff. ## The whole life of a change ``` hack ──► test ──► PR ──► batch-test ──► merge ──► ship ──► release ``` 1. **hack** — edit in place, or let `Super c` agents draft on `worktree-*` branches in parallel; the main checkouts never move. 2. **test** — `./bench try` builds your real machine against the local checkouts (from inside a worktree, against *that* branch). `./bench try switch` activates it — main checkouts only. 3. **PR** — push the branch and open a PR against `main`; never push to or `git merge` into `main` directly. 4. **batch-test** — `./bench try-batch` feels the whole open-PR queue in one rebuild, `main` untouched — so you verify *before* landing anything. 5. **merge** — merge the PRs that passed (on GitHub, or `gh pr merge`); leave the rest open. 6. **ship** — `./bench ship` pushes upstream→downstream, rippling every `flake.lock` in dependency order. 7. **release** — `./bench release `; CI does the rest. Four repos are releasable: **pounce** (GitHub release + Homebrew formula bump), **trill** and **perch** (Homebrew cask bump + the rice's flake pin), and **nebelhaus** (the tag becomes what `nebelhaus.com/init.sh` serves to new installs). **Releases ride tags** Versions are date-based (CalVer). `bench release ` stamps today's date — `YYYY.MM.DD`, or `YYYY.MM.DD-N` on a same-day repeat — into the repo's version source, commits it, and tags `v`. CI does the rest — the GitHub release and the `homebrew-tap` bump. No number is typed by hand, and you never hand-edit the formula's url/sha. And because the installer serves the latest **rice** release, user-visible rice changes aren't really "out" until nebelhaus gets a new tag. Each repo carries its own `CLAUDE.md` with the deep rules for that boundary — start there when working inside one.