Files
nixfiles/docs/shell.md
Emma Thorpe dcc13f94e0
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m21s
docs: move prose documentation into docs/ so the docs site publishes it
The docs-site build syncs this repo's README.md and docs/ into the site
tree; nothing else is copied. All prose apart from the README therefore lived
outside the sync and never appeared on https://docs.lyrapup.pet/nixfiles/, and
the one page that did publish carried 18 link targets that resolved to nothing.

Moves:

  home/README.md           -> docs/shell.md
  home/KEYBINDINGS.md      -> docs/keybindings.md
  hosts/<Name>/README.md   -> docs/hosts/<name>.md

docs/.pages and docs/hosts/.pages give the awesome-pages plugin an explicit
order; new pages are picked up by the trailing '...' without an edit.

Links are rewritten so a single URL is correct in both Gitea and the published
site: absolute Gitea source URLs for .nix files and directories, relative links
between pages under docs/, and absolute docs.lyrapup.pet URLs from the root
README, which the build republishes at a different depth from the rest of the
tree. In-code comments that pointed at a moved README are updated to the new
path.

The README gains a Documentation section covering the sync contract and the
linking rules, and CLAUDE.md carries the short version so future edits do not
reintroduce unsynced pages or dead links.

Verified by reproducing the docs-site assembly locally against its pinned
toolchain (mkdocs 1.6.1, mkdocs-material 9.7.7, awesome-pages 2.10.1): pages
render at the URLs used above and in the declared order.
2026-08-19 17:38:50 +01:00

35 KiB

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/lteza (icons + git), clsclear, 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, Makefile recipes and anything another program execs get the real coreutils binary — nothing that parses output can break;
  • sudo du -sh /var runs the real du: zsh does not expand an alias after sudo;
  • 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

  • grep is not aliased to rg. 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). A grep habit silently producing fewer matches is a worse failure than typing three characters. Type rg.
  • rm is not aliased to trash-put. Retraining rm to mean "recoverable" is a habit that follows you onto every machine where it is not — remote hosts, root shells, containers, CI. Type trash.
  • find is not aliased to fd; the -exec/-print0 vocabulary has no equivalent and scripts lean on it. Type fd.
  • sed is not aliased to sd; sd takes real regex and literal replacements, not sed's expression language. Type sd.
  • 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.devHostName 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/store paths 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