Contributing & worktrees
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.)
The workshop
Section titled “The workshop”The workshop repo 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). Clone it and pull
the family:
git clone https://github.com/nebelhaus/workshop.gitcd workshop./bench cloneYou end up with nebelung/, pounce/, nebelhaus/, and friends as independent
git repos, with the bench script sitting above them.
Where a change goes
Section titled “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
Section titled “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:
# 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:
./bench ship./bench status shows every repo’s git state and every stale lock edge at a
glance.
Parallel agents & worktrees
Section titled “Parallel agents & worktrees”nebelhaus is built to be hacked on by several Claude Code
agents at once. Each agent runs in its own git worktree — an isolated checkout
on a worktree-<name> 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, and it works for any repo.
Two things matter specifically when the repo is a family one:
bench tryis 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 switchrefuses 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 tomainclobber 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 statuslists every agent worktree and unmergedworktree-*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:
workshop_root="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"cd "$(wt child "$workshop_root/nebelung")" # replace nebelung with the repo you needThat 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)
Section titled “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:
./bench try-batch # build every open PR together; prints a tick-off checklist./bench try-batch switch # …and activate the combined tree on this MacVerify 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)
Section titled “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:
zscratch --config config.kdl # try a candidate config.kdlzscratch --layout my-layout.kdl # …or a layoutzscratch --theme nebelung.kdl # …or a themezscratch --plugin tab-bar=tab-bar.wasm # …or swap a freshly-built pluginzscratch clean # kill the scratch session + temp dirFeel 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
Section titled “The whole life of a change”hack ──► test ──► PR ──► batch-test ──► merge ──► ship ──► release- hack — edit in place, or let
Super cagents draft onworktree-*branches in parallel; the main checkouts never move. - test —
./bench trybuilds your real machine against the local checkouts (from inside a worktree, against that branch)../bench try switchactivates it — main checkouts only. - PR — push the branch and open a PR against
main; never push to orgit mergeintomaindirectly. - batch-test —
./bench try-batchfeels the whole open-PR queue in one rebuild,mainuntouched — so you verify before landing anything. - merge — merge the PRs that passed (on GitHub, or
gh pr merge); leave the rest open. - ship —
./bench shippushes upstream→downstream, rippling everyflake.lockin dependency order. - release —
./bench release <repo>; 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 whatnebelhaus.com/init.shserves to new installs).
Each repo carries its own CLAUDE.md with the deep rules for that boundary —
start there when working inside one.