Personal machine setup, managed by Nix home-manager: Emacs, tmux, ghostty, zsh, git, and Claude/Codex agent config. Reproducible across machines (macOS and, in principle, Linux). The Emacs config is plain Elisp, so it works even cloned without Nix. Nix pins the binaries, language servers, and CLI tools.
How this repo (rig), its satellites, and an optional private overlay repo
compose into one machine setup. The core repo is public-safe and defines the
overlay sockets (files sourced or included last); it references no overlay by
name. Work machines currently plug in one overlay, rig-work, which holds
everything employer-specific and always wins by loading last.
Two views, both flowing top to bottom from source repos to installed surfaces.
flowchart TB
subgraph core["~/rig (this repo, public-bound)"]
flake["flake.nix + home.nix<br/>Emacs, CLI tools, language servers"]
emacs["emacs/<br/>init.el, lisp/*.el"]
zshcore["zsh/zshrc.core"]
gitcore["git/gitconfig-core + fragments"]
end
subgraph overlay["private overlay repo (optional; work machines use rig-work)"]
zshwork["zshrc.work"]
gitwork["git/gitconfig-work + identities"]
end
subgraph home["installed surfaces in $HOME"]
cfgemacs["~/.config/emacs (store symlinks)"]
zshrc["~/.zshrc stub"]
gitconfig["~/.gitconfig stub"]
end
flake -->|home-manager switch| cfgemacs
zshcore -->|sourced first| zshrc
zshwork -->|sourced last, wins| zshrc
gitcore -->|included first| gitconfig
gitwork -->|included last, wins| gitconfig
flowchart TB
subgraph core2["~/rig (this repo, public-bound)"]
bootstrap["bootstrap.sh"]
codexdir["claude/ + codex/<br/>personal agent profiles"]
claudeshared["claude-shared/<br/>prose-rules, shared skills"]
end
subgraph tools["~/tools (satellites)"]
csetup["claude-setup<br/>agent profile base: CLAUDE.md,<br/>guard hooks, safety-net, bootstrap"]
sos["save-our-sessions<br/>tmux session recovery"]
tacit["tacit<br/>prose plugin"]
end
subgraph overlay2["private overlay repo (optional; work machines use rig-work)"]
claudework["claude-work/<br/>CLAUDE.md, rules, hooks,<br/>brief supplement"]
codexwork["codex-work/<br/>AGENTS.md, hooks.json"]
installsh["install.sh (idempotent symlinker)"]
end
subgraph home2["profiles in $HOME"]
personalprofile["~/.claude-personal + ~/.codex-personal<br/>personal agent profiles"]
workprofile["~/.claude + ~/.codex<br/>work agent profiles"]
end
bootstrap -->|clones| tools
codexdir -->|home-manager switch| personalprofile
csetup -->|bootstrap.sh| personalprofile
csetup -->|bootstrap.sh, base layer| workprofile
claudework --> installsh
codexwork --> installsh
claudeshared -->|shared + Future Tokens skills| installsh
codexdir -->|shared codex skills| installsh
installsh -->|symlinks on top| workprofile
Layering rules:
- The core repo never contains client names, internal hostnames, or secrets. The overlay repo holds all of those and is never public.
- Overlay config always loads after core config, so work settings win on work machines and their absence is harmless everywhere else.
- Agent profiles follow the same pattern as the shell:
claude-setupinstalls the shared base into each profile dir, then the overlay'sinstall.shsymlinks work-specific instructions and rules on top. - Machine-local, uncommitted state (Emacs
~/.config/emacs-local/, secrets in 1Password) sits outside all three repos.
emacs/- the full Emacs config (plain Elisp, symlinked to~/.config/emacs)tmux/- tmux config + MRU pickers (phase 1: files managed; the running server may still be an older binary until its own cutover)ghostty/- terminal configzsh/-zshrc.core(all shell config) +zshrc.stub(template for~/.zshrc)git/-gitconfig-core(identity, aliases),gitconfig-dirs(personal directory pins),gitconfig-personal, andgitconfig.stubclaude/,claude-shared/,codex/- personal agent-CLI config, tracked as writable out-of-store symlinks (the CLIs rewrite these at runtime; changes show up as git diffs here)bootstrap.sh- fresh-machine setuphome.nix- packages plus all the symlink wiring
~/.zshrc and ~/.gitconfig are NOT symlinks: installers and tools append to
them, and a read-only store symlink would break that. Each is a tiny writable
stub that sources/includes the tracked file, then an optional work-overlay file
(~/.zshrc.work, ~/.gitconfig-work) last, so a work machine's overlay wins.
Work machines get those overlay files from a separate private repo with its own
install.sh; personal machines simply don't have them.
Agent policy used by every profile, work and personal, lives in this repo:
claude-shared/prose-rules.md (each Claude profile's CLAUDE.md includes it)
and codex/skills/ (each codex profile symlinks it). The rule: this repo
holds shared, personal-safe policy; the work overlay holds work-only config.
The overlay may symlink into this repo (bootstrap pins it at ~/rig),
never the reverse. Pick the mechanism deliberately: an @-include when a
profile wraps shared content with its own additions, a symlink when the file
must be identical everywhere.
Portability caveat: some tracked agent config (claude/settings.json,
codex/hooks.json) contains machine-absolute home paths; edit those when
setting up a machine with a different username.
Requires Nix (with flakes) installed and permitted on the target machine.
git clone https://github.com/dabd/rig ~/rig
~/rig/bootstrap.shbootstrap.sh creates the ~/.zshrc / ~/.gitconfig stubs (only if absent),
clones satellite tools to ~/tools, installs oh-my-zsh and its plugins, and
runs the home-manager switch. Idempotent; safe to re-run. The manual
equivalent of just the switch:
cd ~/rig
nix run home-manager/master -- switch --flake .#default --impureflake.nix / flake.lock pinned inputs: nixpkgs, home-manager, emacs-overlay
home.nix packages and the config symlinks
bootstrap.sh fresh-machine setup
zsh/ git/ tmux/ ghostty/ claude/ claude-shared/ codex/ (see above)
emacs/
early-init.el pre-frame tuning + Nix exec-path for GUI .app launches
init.el elpaca bootstrap + module loader
lisp/
ui.el modus-vivendi, which-key, font
completion.el vertico + orderless + marginalia + consult + corfu
editing.el defaults, multiple-cursors, duplicate-line
projects.el project.el, treesit-auto, dired (see naming note below)
lsp.el eglot + dape (Scala / Metals)
git.el magit + forge (github.com; enterprise hosts loaded locally)
llm.el gptel; backend chosen per-machine (work: AWS Bedrock)
The flake ships one machine-agnostic entry,
default. It reads your username from$USERand the platform frombuiltins.currentSystemat switch time, so nothing machine-specific is committed and the same command works on every machine. Reading the environment makes evaluation impure, hence--impure;flake.lockstill pins the toolchain, so what gets installed stays reproducible.mkHomederives the home directory from the OS, so one entry covers macOS and Linux. First switch compiles or fetchesemacs-macport(can be tens of minutes if not cached); later switches are fast.
Launch the GUI via the .app bundle so the window gets proper macOS keyboard
focus (Launch Services). Running the bare binary or a launchd-daemon GUI frame
does not receive focus: the window appears but keystrokes leak to the
terminal (a known macOS activation-policy behavior, worse on macOS 15.x). So:
open -a ~/.nix-profile/Applications/Emacs.app # GUI (primary use)
emacs -nw # quick terminal EmacsConvenience shell functions (ec / et, defined in zsh/zshrc.core):
ec() { open -a "$HOME/.nix-profile/Applications/Emacs.app" "$@"; } # GUI, optional files
et() { "$HOME/.nix-profile/bin/emacs" -nw "$@"; } # terminal frameNo Emacs daemon is used. The
emacs-macportdaemon under launchd cannot serve a focus-correct GUI frame on macOS, so this setup launches the GUI app directly instead (it starts fast).early-init.eladds the Nix profile toexec-path, so the app findsmetals/rg/curleven though a GUI app doesn't inherit the shell PATH.
Two files hold per-machine or work-specific configuration. They live outside
this repo, are never committed, and are created by hand once per machine.
~/.config/emacs is fully Nix-owned (read-only store symlinks), so these live in
~/.config/emacs-local/ instead. Both are loaded with a NOERROR guard, so a
machine without them still starts cleanly.
Registers enterprise GitHub hosts with Forge. Kept out of this public repo because the hostnames are work infrastructure. Example:
;;; local.el --- machine-local: enterprise forge hosts -*- lexical-binding: t; -*-
(with-eval-after-load 'forge
(dolist (h '(("github.example-corp.com" "github.example-corp.com/api/v3"
"github.example-corp.com" "github.example-corp.com")))
(add-to-list 'forge-alist h)))
(provide 'local)Selects the LLM backend for this machine. llm.el ships no backend, so gptel is
inert until this file provides one. On a work laptop with AWS Bedrock:
;;; llm-local.el --- machine-local gptel backend -*- lexical-binding: t; -*-
(with-eval-after-load 'gptel
(require 'gptel-bedrock)
(setq gptel-backend
(gptel-make-bedrock "AWS-Bedrock"
:region "us-east-1"
:model-region 'us
:stream t
:aws-profile "your-bedrock-profile")
gptel-model 'claude-sonnet-4-20250514))
(provide 'llm-local)Bedrock auth uses the AWS profile (SigV4), no API key. It needs a valid AWS
session (e.g. aws sso login) and curl >= 8.9 (provided by home.nix; macOS
ships 8.7). A personal machine could instead point gptel at any other backend
(an API key via auth-source, a local Ollama, etc.).
No secrets are committed. API access is per-machine: work LLM uses AWS Bedrock
via an AWS profile (above); GitHub/Forge tokens resolve through auth-source
(e.g. ~/.authinfo.gpg or the 1Password op CLI), never stored in the repo.
lsp.el uses the built-in eglot. The primary language is Scala via
Metals (metals is in home.nix; scala-ts-mode is the major mode, and
eglot is mapped to launch Metals for it). On first open of a .scala file,
treesit-auto prompts to install the Scala tree-sitter grammar; accept it.
Metals downloads and indexes its build server on first attach, so the first
connection in a project is slow. Add more languages by adding the server to
home.nix and a -ts-mode hook in lsp.el.
The module is projects.el providing feature projects (not project). Emacs
has a built-in project feature; a lisp/project.el that also configures the
built-in causes a recursive-require clash at startup. The rename avoids it;
the module still configures the built-in project package internally.
The normal GUI entry point is the app bundle (see "Launching Emacs" above). A
bare emacs & may start without a window, and a launchd-daemon GUI frame doesn't
get keyboard focus:
open -a ~/.nix-profile/Applications/Emacs.appIf an emacs-macport build ever breaks on a new macOS release, temporarily
install the Homebrew cask (brew install --cask emacs-app) and put
/opt/homebrew/bin ahead of the Nix profile on PATH until the overlay catches up.
~/.gitconfig is the stub described above: it includes the tracked
git/gitconfig-core (personal identity, aliases), then an optional
~/.gitconfig-work overlay, then git/gitconfig-dirs (personal-directory
includeIf pins, loaded last so they win in personal dirs). Magit inherits all
of this by shelling out to git, so Emacs needs no identity config. Enterprise
hosts and work emails stay out of this repo, in the work overlay.