# 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`](./keybindings.md). | Area | Defined in | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | zsh, CLI tools, tmux, ssh, auto-tmux | [`shell.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/shell.nix) | | git (+ delta, commitizen) | [`git.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/git.nix) | | Neovim (nixvim) + LSP | [`editor.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/editor.nix) | | Claude Code (CLAUDE.md, style, memory) | [`claude.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/claude.nix) | | GUI apps, GTK/Firefox theming, cursor | [`desktop.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/desktop.nix) (graphical hosts only) | Shared by every host via [`default.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/default.nix); the work box also layers [`work.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/emmathorpe/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`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/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 ` 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 ` / `\` | | -------- | -------------------- | -------------------------------------------------- | | `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 `exec`s 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 ` 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 ` | prints `` to a terminal instead of dumping the bytes | `hexyl `, 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.`, `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`. One exception to the password: the EDaaS box sets `security.sudo-rs.wheelNeedsPassword = false`. NixOS-WSL ships that default for `security.sudo` — WSL has no console login, so the trust boundary is the Windows session and the Linux account password is never one the user chose — and the option does not carry across to the `security.sudo-rs` module, which defaults to requiring one. Without the explicit setting, `sudo` on that host prompts for a password nobody knows. 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 ` 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): `ff` files, `fg` grep, `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 (`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`, `rn`, `ca`) and the file-tree toggle are listed in [`keybindings.md`](./keybindings.md#neovim). 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 ` (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`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/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/`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/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 (password) | Apple sudo + Touch ID | sudo-rs (passwordless wheel) | | 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 |