From d9464009f03e14b9ae609a51e6781d095624d7e0 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 19 Aug 2026 17:03:07 +0100 Subject: [PATCH 1/2] feat(cli): replace the classic coreutils tools with modern equivalents Adds Rust/Go replacements for the day-to-day utilities and shadows four of them with aliases. Only read-only commands are shadowed (cat, du, df, ps), so a wrong flag costs a retype rather than data; rm, grep, find and sed keep their originals and the replacements are reached by their own names. The aliases land in .zshrc, so they apply to interactive zsh only -- scripts, `sudo ` and anything exec'd by another program still get the real binary. New on every host: dust, dysk, procs, trash-cli, doggo, xh, ouch, jnv, hexyl, fq and tealdeer. dysk is used rather than duf, which is unmaintained upstream. git gains difftastic behind a `git dft` alias. diff.external is deliberately left unset so delta remains the renderer for git diff/show and for anything parsing them. The work box gains kubecolor, aliased over kubectl; it wraps the real kubectl and drops colour when stdout is not a terminal, so pipes are unchanged. home/README.md documents the alias map, the flag incompatibilities (including the two that fail silently: dust -s is --apparent-size, and procs reads a bare `aux` as a search keyword) and the rationale for what was left alone. --- README.md | 3 + home/README.md | 171 ++++++++++++++++++++++++++++++++------ home/git.nix | 13 +++ home/shell.nix | 44 ++++++++++ users/emmathorpe/work.nix | 13 +++ 5 files changed, 219 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d2f188a..47d5a2b 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,9 @@ automatically. - Interactive shell features (zsh, tmux, git, ssh, CLI tools, auto-tmux): [`home/README.md`](./home/README.md). +- Which classic utilities are shadowed by modern replacements, and the flag + differences that will bite: + [`home/README.md` → "Replacing the classics"](./home/README.md#replacing-the-classics). - All Sway / tmux / foot / zsh keyboard shortcuts: [`home/KEYBINDINGS.md`](./home/KEYBINDINGS.md). diff --git a/home/README.md b/home/README.md index 2c27cfd..4ac31e2 100644 --- a/home/README.md +++ b/home/README.md @@ -16,7 +16,7 @@ Keyboard shortcuts have their own reference: [`KEYBINDINGS.md`](./KEYBINDINGS.md Shared by every host via [`default.nix`](./default.nix); the work box also layers [`work.nix`](../users/emmathorpe/work.nix) on top (its own ssh config, extra -packages, and the C#/Helm language servers). The committer identity (name, email, +packages, kubecolor, and the C#/Helm language servers). The committer identity (name, email, signing key) comes from the user registry ([`../users/registry.nix`](../users/registry.nix)), not this module. @@ -35,26 +35,34 @@ signing key) comes from the user registry | 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`. git aliases live in git.nix (below). +**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 | +| 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 @@ -64,6 +72,110 @@ catppuccin upstream themes. (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`. + ## tmux **Auto-start:** opening any interactive terminal — foot, iTerm2, the WSL shell, the @@ -141,13 +253,21 @@ 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 | +| 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 | | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -217,6 +337,7 @@ Claude to route new memories there. | | 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) | | 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) | diff --git a/home/git.nix b/home/git.nix index 9bdad1c..fb508c2 100644 --- a/home/git.nix +++ b/home/git.nix @@ -74,6 +74,11 @@ in # `cz commit`, `git cz bump`, etc. `git cc` is a shortcut for the prompt. cz = "!cz"; cc = "!cz commit"; + # Structural (syntax-aware) diff, on demand. Set per-invocation via the + # environment rather than `diff.external`, which would also change what + # `git show` and `git log -p --ext-diff` emit for every caller. + # Takes the same arguments as `git diff`: `git dft HEAD~3 -- file`. + dft = "!GIT_EXTERNAL_DIFF=difft git diff"; }; # SSH signing, key from the registry. mkDefault so a host lacking the key @@ -99,6 +104,14 @@ in enableGitIntegration = true; }; + # difftastic backs the `dft` alias above. git.enable stays off on purpose: + # the module's git integration sets `diff.external`, which would displace + # delta as the diff renderer everywhere instead of only where asked. + programs.difftastic = { + enable = true; + git.enable = false; + }; + # lazygit: TUI for staging/rebasing, themed to Catppuccin Mocha to match. programs.lazygit = { enable = true; diff --git a/home/shell.nix b/home/shell.nix index ecba544..5f7c0e4 100644 --- a/home/shell.nix +++ b/home/shell.nix @@ -26,8 +26,32 @@ in pkgs.tea pkgs.hyperfine # command-line benchmarking pkgs.sd # saner find-and-replace than sed + + # Replacements for the classic coreutils/BSD tools. Only the read-only ones + # are aliased over the original name (see shellAliases below); the rest keep + # their own name so nothing changes shape under a script's feet. The alias + # map and the flag-compatibility differences are documented in + # ./README.md, "Replacing the classics". + pkgs.dust # du: tree-shaped, size-sorted disk usage + pkgs.dysk # df: mounted filesystems (duf is unmaintained upstream) + pkgs.procs # ps: process list with tree, ports and container columns + pkgs.trash-cli # rm: XDG trash; `trash` / `trash-list` / `trash-restore` + pkgs.doggo # dig: DNS lookups + pkgs.xh # curl, for interactive HTTP poking (curl stays for scripts) + pkgs.ouch # tar/unzip/7z/zstd: one command for every archive format + pkgs.jnv # interactive jq filter builder (jq itself stays for scripts) + pkgs.hexyl # hex viewer + pkgs.fq # jq for binary formats ]; + # tldr pages: worked examples for a command, next to (not instead of) man. + # enableAutoUpdates defaults on and installs a tldr-update user timer, which + # keeps the page cache fresh -- without it `tldr` fails until first `--update`. + programs.tealdeer = { + enable = true; + settings.display.compact = true; + }; + # Resource monitor, themed Catppuccin Mocha to match the rest of the desktop. # btop does not bundle the theme, so vendor it from catppuccin/btop (pinned). programs.btop = { @@ -137,6 +161,26 @@ in la = "eza --icons --git -la"; lt = "eza --icons --git --tree"; cls = "clear"; + + # Shadow the classics with their modern equivalents. Only read-only + # commands are shadowed: a wrong flag costs a retype, never data. The + # flag vocabularies are NOT compatible (`du -sh`, `df -h`, `ps aux` all + # fail here) -- see ./README.md, "Replacing the classics". + # + # Blast radius is bounded by where these live: shellAliases lands in + # .zshrc, so only interactive zsh sees them. Scripts, `sudo ` and + # anything exec'd by another program still get the real binary. To reach + # the original in an interactive shell: `command du` or `\du`. + cat = "bat --paging=never"; # bat is already the PAGER/MANPAGER + du = "dust"; + df = "dysk"; + ps = "procs"; + + # `rm` is deliberately NOT aliased to trash-put. Retraining `rm` to mean + # "recoverable" is a habit that follows you onto machines where it does + # not (every remote host, every root shell, every container), and trash + # semantics break down anyway on a different filesystem or on + # root-owned paths. Type `trash` when you want a trash can. }; }; diff --git a/users/emmathorpe/work.nix b/users/emmathorpe/work.nix index ef24022..08cb887 100644 --- a/users/emmathorpe/work.nix +++ b/users/emmathorpe/work.nix @@ -50,6 +50,19 @@ ]; services.ssh-agent.enable = true; + # Colourised kubectl. enableAlias points `kubectl` at kubecolor, which parses + # the output of the real kubectl underneath and passes anything it does not + # recognise straight through, so every flag and subcommand still works. It + # drops colour automatically when stdout is not a terminal, leaving pipes into + # grep/jq/yq byte-identical. zsh integration reuses kubectl's own completions. + # Note the alias does apply to `KUBECONFIG=... kubectl ...`: zsh expands + # aliases after a variable-assignment prefix. + programs.kubecolor = { + enable = true; + enableAlias = true; + enableZshIntegration = true; + }; + # gcx (above) keeps its OAuth tokens in the system keychain and has no # plaintext fallback, so this WSL box needs something owning # org.freedesktop.secrets. See home/secret-service.nix for why -- 2.54.0 From dfafac8de980b4a73dbeab155dc104cd781a879f Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 19 Aug 2026 17:03:15 +0100 Subject: [PATCH 2/2] feat(security): swap sudo for the memory-safe sudo-rs 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. --- README.md | 2 +- home/README.md | 32 ++++++++++++++++++++++++-------- modules/common-nixos.nix | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 47d5a2b..47a6de9 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ module reaches a host: **baseModules** (every NixOS host, via `flake.nix`), | Module | Imported by | What it does / when to use it | | ------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `common-nixos.nix` | baseModules (all NixOS) | Timezone/locale, store hygiene (auto-optimise, big download buffer, **no** auto-GC), the nix-community binary cache, `nix-ld`, base CLI (`git`, `fastfetch`), and the fleet-wide font stack. | +| `common-nixos.nix` | baseModules (all NixOS) | Timezone/locale, store hygiene (auto-optimise, big download buffer, **no** auto-GC), the nix-community binary cache, `nix-ld`, **sudo-rs** in place of sudo, base CLI (`git`, `fastfetch`), and the fleet-wide font stack. | | `users.nix` | baseModules (all NixOS) | Builds `users.users` from the registry for the host's `hostUsers`; enables zsh; enables Firefox + Thunderbird **only** when `features.swayDesktop.enable` is on. Applies per-user `linger`. | | `features.nix` | baseModules (all NixOS) | Declares the feature-flag options (`features.swayDesktop.enable`, `features.claudeCode.enable`) so any host can read/set them without importing the heavy implementation module, plus the CPU capability fact they derive from (`features.cpu.microarchLevel`) and the assertion that guards it. See "CPU capability gating". | | `workstation.nix` | transitively (via laptop/desktop) | Form-factor-agnostic base for physical graphical hosts: turns on `swayDesktop`, Dvorak console, PipeWire, firewall (default-deny), fstrim, earlyoom, fwupd, thermald (x86), redistributable fw. | diff --git a/home/README.md b/home/README.md index 4ac31e2..ca37142 100644 --- a/home/README.md +++ b/home/README.md @@ -176,6 +176,21 @@ 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 @@ -334,11 +349,12 @@ Claude to route new memories there. ## 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) | -| 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 | +| | 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 | diff --git a/modules/common-nixos.nix b/modules/common-nixos.nix index e4b0f00..9ba118b 100644 --- a/modules/common-nixos.nix +++ b/modules/common-nixos.nix @@ -25,6 +25,22 @@ # toolchains, language-server downloads) on every NixOS host, not just WSL. programs.nix-ld.enable = true; + # Memory-safe sudo. The two modules assert against being enabled together; + # this one sets `security.sudo.enable = false` via mkDefault, so it is a + # straight swap and not an addition. + # + # Safe here because this fleet only ever uses the stock policy -- wheel may + # run anything, with a password -- which sudo-rs implements completely. It + # does not cover the more exotic sudoers surface (host aliases, LDAP/SSSD + # sudoers, most `Defaults` settings, `sudoreplay`); adding any of those means + # going back to `security.sudo`. + # + # Recovery if a host ever refuses to escalate: get a root shell without sudo + # (`wsl -u root -d NixOS` on the WSL box, the console or a serial/HDMI login + # elsewhere) and roll back -- `nixos-rebuild switch --rollback`, or pick the + # previous generation from the boot menu. + security.sudo-rs.enable = true; + # Minimal system-level CLI available before the home-manager profile loads # (e.g. early boot / rescue). User-level tooling lives in home-manager. environment.systemPackages = with pkgs; [ -- 2.54.0