security.sudo-rs.enable sets security.sudo.enable = false via mkDefault, so this is a straight swap; the two modules assert against being on together. The fleet only uses the stock policy -- wheel may run anything, with a password -- which sudo-rs implements fully. It does not cover host aliases, LDAP/SSSD sudoers, sudoreplay or most Defaults settings; needing any of those means reverting to security.sudo. The macOS host is unaffected and keeps Apple's sudo with Touch ID. Recovery from a host that will not escalate is documented in the module and in home/README.md: get a root shell that does not go through sudo, then roll back the generation.
Interactive shell environment
Everything the shell, terminal multiplexer, git and ssh do beyond their defaults, and where each is defined. All of it is managed declaratively through home-manager — edit the listed file and rebuild, never the generated dotfiles.
Keyboard shortcuts have their own reference: KEYBINDINGS.md.
| Area | Defined in |
|---|---|
| zsh, CLI tools, tmux, ssh, auto-tmux | shell.nix |
| git (+ delta, commitizen) | git.nix |
| Neovim (nixvim) + LSP | editor.nix |
| Claude Code (CLAUDE.md, style, memory) | claude.nix |
| GUI apps, GTK/Firefox theming, cursor | desktop.nix (graphical hosts only) |
Shared by every host via default.nix; the work box also layers
work.nix on top (its own ssh config, extra
packages, kubecolor, and the C#/Helm language servers). The committer identity (name, email,
signing key) comes from the user registry
(../users/registry.nix), not this module.
zsh
| Feature | Notes |
|---|---|
| oh-my-zsh | plugins git, man, sudo (Esc-Esc to prepend sudo), colored-man-pages, extract; theme robbyrussell |
| Autosuggestion | fish-style history suggestions as you type (→ to accept) |
| Syntax highlighting | commands coloured by validity as you type |
| Completion | menu completion; the dump is rebuilt on every activation (see Maintenance) |
| History | 100k in-memory/on-disk, deduped, space-prefixed commands ignored, timestamped, shared live across sessions; file stays at ~/.zsh_history |
| Dotfiles location | dotDir is ~/.config/zsh (XDG) — .zshrc/.zshenv/.zcompdump live there; ~/.zshenv only bootstraps $ZDOTDIR |
| History substring search | type a fragment, then ↑/↓ cycles matching past commands — works in foot, iTerm2 and the Linux TTY (both CSI and SS3 arrow encodings bound) |
| Prompt | hostname is prefixed when over SSH |
Aliases: ls/ll/la/lt → eza (icons + git), cls → clear,
cat/du/df/ps → their modern equivalents (see "Replacing the classics").
git aliases live in git.nix (below).
CLI tools
| Tool | What it gives you |
|---|---|
fzf |
Ctrl-R fuzzy history, Ctrl-T file picker, Alt-C fuzzy cd (Catppuccin-themed) |
zoxide |
z <fragment> jumps to frecent directories |
direnv + nix-direnv |
per-project environments auto-loaded on cd (cached Nix dev shells) |
eza |
modern ls (drives the ls aliases) |
bat |
syntax-highlighting pager (Catppuccin Mocha theme); behaves like cat when piped; also the MANPAGER |
ripgrep / fd |
fast search (rg) and find (fd); also back fzf |
jq |
JSON processor |
gh / tea |
GitHub and Gitea (code.emmathe.dev) CLIs; gh uses SSH |
nix-index |
command-not-found: an unknown command tells you which Nix package provides it (prebuilt DB, no manual indexing) |
comma (,) |
run an uninstalled program once: , cowsay hi |
nh |
nicer nixos-rebuild/home-manager with diffs; $NH_FLAKE set to the repo. No scheduled GC (it could reap paths a running generation still references) — collect garbage manually with nh clean all / nix-collect-garbage -d |
btop |
resource monitor, themed Catppuccin Mocha (vendored theme) |
lazygit |
git TUI for staging/rebasing, themed to match (git.nix) |
hyperfine / sd |
command-line benchmarking; saner find-and-replace than sed |
tldr (tealdeer) |
worked examples for a command, alongside man; the page cache is refreshed by a tldr-update user timer |
jnv / fq |
interactive jq-filter builder for JSON; jq syntax over binary formats (ELF, PNG, gzip, mp4…) |
hexyl |
hex viewer, coloured by byte class |
ouch |
one command for every archive format (ouch d/c/l) |
dust dysk procs |
du / df / ps replacements — aliased over the originals, see below |
trash-cli doggo xh |
rm (to the XDG trash) / dig / curl replacements — not aliased, see below |
Theming: fzf, bat, btop, lazygit and git's delta pager are all
Catppuccin Mocha, driven from the shared ../lib/catppuccin-mocha.nix palette / the
catppuccin upstream themes.
Env & defaults: xdg.enable on; PAGER/MANPAGER (bat) set in default.nix
(the editor owns $EDITOR/$VISUAL); xdg.mimeApps maps web→Firefox,
directories→nemo (desktop.nix).
Replacing the classics
Muscle memory is the expensive part of this, not the packages. Four commands are shadowed — the old name now runs a new tool. Everything else keeps a new name, so the original is never displaced.
Shadowed by an alias
| You type | You now run | The original is still command <name> / \<name> |
|---|---|---|
cat |
bat --paging=never |
command cat |
du |
dust |
command du |
df |
dysk |
command df |
ps |
procs |
command ps |
Only read-only commands are shadowed, so the worst case of a wrong flag is a
retype rather than lost data. rm, grep, curl and find are deliberately
left alone — see "Left alone on purpose" below.
Where the aliases apply. They are written into ~/.config/zsh/.zshrc, so
they exist only in an interactive zsh:
- shell scripts,
Makefilerecipes and anything another programexecs get the real coreutils binary — nothing that parses output can break; sudo du -sh /varruns the realdu: zsh does not expand an alias aftersudo;KUBECONFIG=… kubectl …does expand — zsh expands aliases after a variable-assignment prefix. That is what makes the kubecolor alias on the work box (below) useful rather than a special case you have to remember.
Flag gotchas
These replacements are not drop-in. The two marked silent are the dangerous ones — they succeed and answer a different question than the one you asked. Everything else fails loudly.
| Old habit | What happens now | Do this instead |
|---|---|---|
du -sh dir |
dust prints its usage and exits non-zero — -h is not a dust flag |
dust dir (units are human by default; the total is the last row) |
du -s dir |
silent: dust's -s is --apparent-size, not --summarize |
dust -d 0 dir for a single total line |
du --max-depth=2 |
not recognised | dust -d 2 |
df -h |
dysk rejects -h |
dysk (SI units by default; -u binary for 1024-based) |
df -i |
not recognised | dysk -c +inodes |
df -a |
works, same meaning (all mount points) | — |
df /some/path |
works, same meaning (the device holding that path) | — |
ps aux |
silent: aux is read as a search keyword, so you get only processes whose command line contains the string "aux" |
procs lists everything; procs <pattern> filters |
ps -ef |
error: unexpected argument '-e' |
procs |
ps -p 1234 |
not recognised | procs 1234 |
procs -a |
silent: -a is --and (combine search keywords), not "all" |
drop it — procs already shows everything |
cat -v / cat -e |
error: unexpected argument |
cat -A does work (bat implements show-all); else command cat -v |
cat -n |
works, but bat's number column, not coreutils' layout | fine to read; command cat -n when the exact layout matters |
cat <binary> |
prints <BINARY> to a terminal instead of dumping the bytes |
hexyl <file>, or command cat to dump |
Useful new capabilities in the same tools: procs --tree, procs --watch,
dust -r (largest at the top), dysk -s size, dysk -f 'type=ext4'.
Piping is safe for cat. bat drops all decoration and colour when stdout is
not a terminal, so cat f | sha256sum is byte-for-byte what coreutils cat
would have given. The others are TUI-shaped tables with no stable format — if
something needs to parse them, use dysk --json/--csv, procs --json, or the
original binary.
Renamed, not shadowed
| Instead of | Use | Notes |
|---|---|---|
rm |
trash |
Moves to the XDG trash. trash-list, trash-restore (interactive picker), trash-empty [days]. It never deletes in place: if it cannot create a trash directory on that filesystem it errors out. |
dig / nslookup |
doggo |
doggo example.com MX @1.1.1.1; --json for scripting. Not aliased — dig (from the bind closure that other modules pull in) stays where scripts expect it. |
curl (interactive poking) |
xh |
HTTPie syntax: xh POST api.example/x name=lyra. xhs is xh --https. curl stays installed and unaliased — it is what scripts and CI use. |
tar / unzip / 7z |
ouch |
ouch d file.<anything>, ouch c out.tar.zst src/, ouch l archive. Format is inferred from the extension. The oh-my-zsh extract function still works too. |
jq (exploring a payload) |
jnv |
Interactive filter builder over a JSON file; it prints the jq expression you built. jq remains the scripting tool. |
hexdump -C / xxd |
hexyl |
hexyl -n 256 -s 0x40 file for a window into a large file. |
strings on a known format |
fq |
jq syntax over binary formats: fq -d elf '.sections[].name' ./bin. |
| skimming a man page | tldr |
Worked examples. man is untouched (and still rendered through bat). |
Left alone on purpose
grepis not aliased torg. ripgrep is recursive by default, skips gitignored and hidden files, and uses a different regex dialect (no backreferences, no POSIX classes in the same form). Agrephabit silently producing fewer matches is a worse failure than typing three characters. Typerg.rmis not aliased totrash-put. Retrainingrmto mean "recoverable" is a habit that follows you onto every machine where it is not — remote hosts, root shells, containers, CI. Typetrash.findis not aliased tofd; the-exec/-print0vocabulary has no equivalent and scripts lean on it. Typefd.sedis not aliased tosd;sdtakes real regex and literal replacements, not sed's expression language. Typesd.- coreutils itself is not swapped for
uutils-coreutils. It is packaged and tempting, but every Nix builder and shell script on these hosts is written against GNU behaviour, including its forty-year-old edge cases.
Work box only: kubectl → kubecolor
On EDaaS (work.nix) kubectl is aliased to kubecolor, which runs the real
kubectl underneath and colourises what comes back. Nothing to relearn: every
flag, subcommand and plugin passes straight through, unrecognised output is
printed verbatim, and colour is dropped automatically when stdout is not a
terminal — so kubectl get -o json … | jq is unchanged. The alias also applies
to KUBECONFIG=prodconfig kubectl …, per the alias-expansion note above.
Completions are kubectl's own (compdef kubecolor=kubectl). Escape hatch as
ever: command kubectl.
sudo → sudo-rs
Every NixOS host now uses sudo-rs, the memory-safe reimplementation, in
place of sudo (modules/common-nixos.nix; the macOS host keeps Apple's sudo
with Touch ID). Day to day there is nothing to learn — sudo, sudo -i,
sudo -u, sudo -l, sudoedit and visudo all behave as before against this
fleet's stock "wheel, with a password" policy. What it does not implement:
host aliases, LDAP/SSSD sudoers, sudoreplay, and most Defaults settings.
Needing any of those means reverting to security.sudo.
If a host ever refuses to escalate, get a root shell that does not go through
sudo (wsl -u root -d NixOS on the work box; the console or a serial/HDMI login
elsewhere) and roll back with nixos-rebuild switch --rollback, or pick the
previous generation from the boot menu.
tmux
Auto-start: opening any interactive terminal — foot, iTerm2, the WSL shell, the
Linux console — drops you straight into a tmux session named main (attach if it
exists, else create). Panes run a plain non-login zsh. It deliberately does not
fire for SSH sessions, VS Code's integrated terminal, already-inside-tmux, or
non-interactive shells. Escape hatch: NO_TMUX=1 <terminal> opens a bare shell.
| Setting | Value |
|---|---|
| Mode keys | vi |
| Mouse | on |
| Scrollback | 500000 lines |
escape-time |
10ms (the 500ms default lagged vim's ESC) |
focus-events |
on (vim autoread) |
base-index / pane-base-index |
1 |
| Splits | prefix s vertical, prefix v horizontal (stock %/" unbound) |
| Pane nav | Alt+arrows (no prefix) |
| Terminal | default-terminal tmux-256color; truecolor advertised per outer terminal (foot*, xterm-256color/iTerm2) via terminal-features … RGB |
| Clipboard | set-clipboard on; foot terminal-features advertise truecolor/sync/OSC52/title/cursor |
Plugins: sensible, vim-tmux-navigator (Ctrl-h/j/k/l across vim ↔ tmux),
yank, extrakto (prefix+Tab: fzf-grab paths/URLs/text from the pane into
the prompt), catppuccin (Mocha statusline), resurrect + continuum
(sessions auto-save and restore across reboots). The statusline draws Nerd-Font
glyphs — see Fonts.
Fonts
JetBrainsMono Nerd Font, Noto Sans and Noto Color Emoji are
installed on every host (in common-nixos.nix, because tmux/terminals run
everywhere; the Mac installs the Nerd Font to /Library/Fonts via the Darwin
config). fonts.fontconfig.defaultFonts maps the generic families so anything
asking for monospace gets the Nerd Font (with emoji fallback) — this also
gives the WSL box emoji/sans coverage it otherwise lacked. foot uses the Nerd
Font as its main font automatically. iTerm2's font is a GUI setting — set it to
JetBrainsMono Nerd Font (Settings → Profiles → Text → Font) so the tmux
statusline glyphs render instead of ?.
Editor (Neovim)
nvim — aliased to vi/vim, and set as $EDITOR/$VISUAL — is configured
declaratively with nixvim, so the same plugins and config are baked in on
every host. Migrated from plain vim; the practical gain is a real LSP stack in
place of the old (inert) ALE.
| Feature | Notes |
|---|---|
| Colorscheme | Catppuccin Mocha (matches the terminal and the rest of the desktop) |
| File tree | nvim-tree, toggled with ,, (comma twice; was nerdtree) |
| Fuzzy finder | telescope (+fzf-native): <leader>ff files, <leader>fg grep, <leader>fb buffers |
| Format on save | conform-nvim (nixfmt, stylua, ruff, shfmt, prettier, gofumpt; LSP fallback otherwise) |
| Git | fugitive (:Git …) + gitsigns gutter signs/blame |
| Diagnostics | inline + trouble list (<leader>xx) |
| Completion | nvim-cmp (LSP/buffer/path) with luasnip snippet expansion |
| Indent guides | indent-blankline, on by default (was vim-indent-guides) |
| Statusline | lualine (Catppuccin theme) |
| Editing | which-key hints, comment (gc/gcc), autopairs, treesitter textobjects |
| Pane nav | vim-tmux-navigator — Ctrl+h/j/k/l moves across vim splits and tmux panes |
| Syntax | tree-sitter (nix, lua, bash, markdown, groovy, c#, python, terraform, yaml) |
| LSP | nvim-cmp completion + servers nil_ls (Nix), lua_ls, pyright (Python), terraformls |
| Indentation | 2-wide hard tabs (noexpandtab, tabstop/shiftwidth = 2); line numbers on |
| Filetypes | *Jenkinsfile → groovy |
Leader is Space. LSP keymaps (gd, gr, K, <leader>rn, <leader>ca) and
the file-tree toggle are listed in
KEYBINDINGS.md. Add a universal language server by
enabling it under programs.nixvim.plugins.lsp.servers in editor.nix;
host-specific ones go in that host's module — the work box (work.nix) adds
omnisharp (C#) and helm_ls (Helm), kept off the personal machines.
git
Pager is delta. commitizen is installed on every host; cz defaults to
Conventional Commits. lazygit (themed) is the TUI. The commit-graph is kept
current (gc/fetch.writeCommitGraph) so lg stays fast.
| Aliases | |
|---|---|
st co sw br ci |
status / checkout / switch / branch / commit |
last unstage |
last commit / unstage |
amend fixup undo |
amend-no-edit / commit --fixup / soft-reset HEAD~1 (keep staged) |
lg |
graph log, all branches |
cz cc |
git cz <sub> (e.g. git cz c) and git cc → commitizen prompt |
dft |
structural (syntax-aware) diff via difftastic; takes git diff arguments |
git dft vs git diff. delta stays the default renderer for everything;
diff.external is deliberately not set, so git diff, git show and
anything parsing their output are unchanged. Reach for dft when a refactor
moved code around and a line-based diff is noise. One wrinkle: dft is a
!-shell alias, and git runs those from the repository root — pass pathspecs
relative to the root, not to your current directory.
| Behaviour | |
|---|---|
| Pulls | rebase, with autostash + autosquash |
| Fetch | prune deleted remote branches |
| Conflicts | zdiff3 (shows the common ancestor) |
| Diffs | histogram algorithm, colour-moved |
rerere |
remembers + replays conflict resolutions |
| Commit editor | full diff shown (commit.verbose) |
| Misc | branches sorted by date, column.ui = auto, help.autocorrect = prompt, push.autoSetupRemote |
| Global ignores | result, result-*, .direnv, *.swp, .DS_Store |
| Signing | SSH commit + tag signing (mkDefault, so a host without the key in its agent can disable it). Name, email and signing key all come from the per-user identity (the user registry, ../users/registry.nix). |
ssh
| Feature | Notes |
|---|---|
| ssh-agent | runs on Linux (launchd on macOS); keys added on first use so the passphrase is typed once per login session — this also feeds git commit signing |
| macOS | UseKeychain caches the passphrase in the login keychain (guarded by IgnoreUnknown, so a non-Apple ssh skips it instead of erroring) |
| Gitea remote | code.emmathe.dev → HostName 10.187.1.76 (DNS-override), Port 30009, user git, dedicated key, identitiesOnly |
| Defaults | the module's deprecated default block is opted out; equivalents kept under settings."*" |
The work box keeps its own ~/.ssh/config (home-manager's programs.ssh is
forced off there) but still runs the agent.
Claude Code
Managed declaratively by claude.nix on every host whose CPU
can run it (the CLI is pkgs.claude-code, tracked to unstable via the flake
overlay).
Capability gate. The module installs nothing — CLI or files — when
osConfig.features.claudeCode.enable is off. That flag is derived fleet-wide
from the host's declared CPU level (see "CPU capability gating" in the root
README): the Node runtime needs SSE4.2/POPCNT, so anything below x86-64-v2 (the
Mac Pro 3,1) is excluded. Hosts that do not define the option — the Darwin host
and the standalone homeConfigurations — keep it enabled.
| Managed (static, from Nix) | Left mutable (runtime state) |
|---|---|
~/.claude/CLAUDE.md (persona + memory workflow) |
settings.json (permissions, model, theme, /config) |
~/.claude/output-styles/soviet-engineer.md |
.credentials.json, history, caches |
~/.claude/memory/ (read-only symlink to the repo) |
settings.json is intentionally not managed: Claude rewrites it at runtime
(interactive permission grants, /config), which a read-only store symlink would
break.
Memory is sourced from this repo. The files in
claude/memory/ are the source of truth; they are symlinked
read-only into ~/.claude/memory, so recall works but the runtime "save a
memory" path does not. To add/change/remove a memory, edit claude/memory/
(one file per memory + the MEMORY.md index) and rebuild — CLAUDE.md tells
Claude to route new memories there.
Maintenance behaviours
- zcompdump reset —
~/.config/zsh/.zcompdump*(plus legacy~/.zcompdump*and the cache copy) is removed on every activation, so a stale dump (pointing at/nix/storepaths a rebuild or a manual GC removed) can't break completion with_git: function definition file not found. - GC — no scheduled timer; collect garbage deliberately (
nh clean all/nix-collect-garbage -d) when no important session is running.
Per-host differences
| Personal Linux (sway) | macOS | Work WSL (EDaaS) | |
|---|---|---|---|
| Auto-tmux | yes (foot/TTY) | yes (iTerm2) | yes (WSL shell) |
kubectl → kubecolor |
no (no kubectl) | no | yes (work module) |
sudo implementation |
sudo-rs | Apple sudo + Touch ID | sudo-rs |
| git email | iam@emmathe.dev |
iam@emmathe.dev |
…@citrix.com (work) |
| ssh config managed | yes | yes | no (keeps corporate config) |
| ssh-agent | yes | launchd | yes (work module) |
| GUI / theming (desktop.nix) | yes | no | no |