Author SHA1 Message Date
Emma ThorpeandClaude Opus 5 a94a749f29 feat(hosts): add the Raspberry Pi Zero 2 W Psion sidecar
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m16s
A headless aarch64 companion for a Psion 5MX: PPP over RS232 with NAT out to
wifi and a telnet login, plus a cleartext POP3/SMTP proxy for the Psion's mail
client.

- hosts/PiZero2W/: host config, serial-ppp.nix, email-proxy.nix, an SD-image
  variant, and a hardware-configuration.nix placeholder.
- Host table entry on nixos-hardware's raspberry-pi-3 profile; the Zero 2 W is
  the Pi 3's BCM2837 SoC. nixpkgs' linuxPackages_rpi02w is deprecated and warns
  that the linux-rpi series is being removed in favour of nixos-hardware.
- The host owns its firmware partition (hardware.raspberry-pi.firmware), which
  is what puts the disable-bt and uart0/ctsrts overlays in config.txt so
  /dev/ttyAMA0 is the RS232 header rather than Bluetooth. uboot.enable keeps the
  U-Boot -> extlinux boot path the rewritten config.txt would otherwise lose.
- packages.aarch64-linux.zero2w-sd-image: the host's own configuration as an
  installable card. The board has no Ethernet and no free serial port, so a
  generic image would leave no way in.
- The mail proxy comes from the legacy-email-proxy flake, which provides the
  package and the NixOS module; nothing about it is vendored here.
- docs/hosts/pizero2w.md, plus README host table and shared-layer notes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 13:38:27 +01:00
lyrathorpe 1ff333a896 Merge pull request 'fix(docs): pin the site section title so it renders as nixfiles' (#97) from fix/docs-section-title into main
CI / flake (push) Successful in 6m33s
Reviewed-on: #97
2026-08-19 18:24:40 +01:00
Emma Thorpe 9d199bc087 fix(docs): pin the site section title so it renders as nixfiles
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 1m10s
With no entry in the docs-site nav (removed there so awesome-pages can
discover the synced trees), MkDocs derives the section name from the directory
and title-cases it, rendering "Nixfiles". The previous hardcoded nav spelled it
lowercase. Setting title in docs/.pages restores that without reintroducing a
nav entry.

Verified by rebuilding the aggregated site locally with both source trees
synced as the workflow does.
2026-08-19 18:15:48 +01:00
lyrathorpe c7adcccbb3 Merge pull request 'docs: publish the prose documentation to docs.lyrapup.pet' (#96) from docs/publish-to-docs-site into main
CI / flake (push) Successful in 4m9s
Reviewed-on: #96
2026-08-19 17:51:40 +01:00
Emma Thorpe dcc13f94e0 docs: move prose documentation into docs/ so the docs site publishes it
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m21s
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
lyrathorpe d30d8f9892 Merge pull request 'feat(cli): modern replacements for the classic coreutils tools, and sudo-rs' (#95) from feat/modern-cli-replacements into main
CI / flake (push) Successful in 4m4s
Reviewed-on: #95
2026-08-19 17:21:40 +01:00
Emma Thorpe dfafac8de9 feat(security): swap sudo for the memory-safe sudo-rs
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m6s
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.
2026-08-19 17:03:15 +01:00
Emma Thorpe d9464009f0 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 <cmd>` 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.
2026-08-19 17:03:07 +01:00
lyrathorpe d654eac1e2 Merge pull request 'feat(macpro31): NVIDIA P400 with CUDA Docker, and a fleet-wide CPU capability gate' (#94) from feat/macpro31-nvidia-cuda into main
CI / flake (push) Successful in 4m44s
Reviewed-on: #94
2026-08-17 20:59:21 +01:00
Emma Thorpe d4e7475db9 fix(macpro31): load the NVIDIA modules and guard the CDI generator
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m12s
The CDI generator aborted with "failed to initialize NVML: Driver Not
Loaded", taking docker.service with it (requiredBy) and failing the
switch.

Two causes. The nixpkgs NVIDIA module only adds nvidia/nvidia_modeset/
nvidia_drm to boot.kernelModules when services.xserver.enable is set,
which is false on this Wayland-only host, so load them explicitly.
nvidia_uvm stays out: the module's modprobe softdep loads it once the GPU
device exists.

The generator also runs during activation, when a module rebuilt against a
new kernel cannot be loaded until reboot -- a guaranteed failure after
every kernel bump. Guard it with ConditionPathExists on
/proc/driver/nvidia/version so it skips rather than fails; the toolkit's
udev rule restarts it when the device appears, so the specs are generated
on the next boot.
2026-08-17 20:47:33 +01:00
Emma Thorpe 0f7fb7f78a feat(macpro31): NVIDIA Quadro P400 driver and CUDA-enabled Docker
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m11s
The stock GPU has been replaced with a Quadro P400 (Pascal, GP108). Add
hosts/MacPro31/nvidia.nix:

- Driver branch 580 (nvidiaPackages.legacy_580), not the nixpkgs default
  production branch (595.x). 580 is the last branch supporting
  Maxwell/Pascal/Volta and is an LTS branch until Aug 2028; a newer one
  does not drive this card.
- modesetting.enable for Wayland (nvidia-drm.modeset=1), open = false
  (the open kernel modules need Turing or later), and sway
  --unsupported-gpu, which wlroots requires with the proprietary driver.
- Docker with GPU access via CDI (hardware.nvidia-container-toolkit),
  rather than the deprecated virtualisation.docker.enableNvidia runtime
  wrapper. Containers run with --device=nvidia.com/gpu=all and must ship
  a CUDA 12.x or older runtime: CUDA 13 dropped sm_61.

The driver packages are unfree, so allowlist them in unfreePackages; they
are not cached and the kernel module builds on the host.

Also declare features.cpu.microarchLevel = 1 for this machine: the
Harpertown Xeons have SSE4.1 but no SSE4.2/POPCNT, which switches off
Claude Code through the fleet-wide gate.
2026-08-17 20:35:39 +01:00
Emma Thorpe 0d13581896 feat(features): gate Claude Code on the host CPU microarchitecture level
Claude Code runs on Node, whose V8 build requires SSE4.2 and POPCNT
(x86-64-v2). On an older x86_64 CPU it does not run, so it must not be
installed there in the first place.

Nix cannot detect the CPU (pure evaluation, hosts often built elsewhere),
so add features.cpu.microarchLevel: the psABI level a host declares about
itself, defaulting to 2. features.claudeCode.enable derives from it, and
home/claude.nix reads that through home-manager's osConfig and installs
nothing -- CLI, CLAUDE.md, output style or memory symlink -- when it is
off. Hosts without the option (Darwin, the standalone homeConfigurations)
keep the tool enabled.

An assertion fails evaluation if a host force-enables the flag below the
required level, so the mistake surfaces in nix flake check rather than as
an illegal-instruction crash on the machine.
2026-08-17 20:35:29 +01:00
renovate-bot 526e6a08e2 Merge pull request 'chore(deps): lock file maintenance flake inputs' (#93) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 3m58s
2026-08-17 01:07:10 +01:00
Renovate Bot 9e749cce2b chore(deps): lock file maintenance flake inputs
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 4m28s
2026-08-17 00:02:26 +00:00
renovate-bot 1766fb7b3f Merge pull request 'chore(deps): lock file maintenance flake inputs' (#92) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 7m14s
2026-08-17 00:14:19 +01:00
Renovate Bot dba73e1199 chore(deps): lock file maintenance flake inputs
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 9m17s
2026-08-16 23:04:32 +00:00
renovate-bot e9835372cd Merge pull request 'chore(deps): update gitea actions to 13d8dd5' (#91) from renovate/gitea-actions into main
CI / flake (push) Successful in 3m58s
2026-08-13 16:05:40 +01:00
Renovate Bot bd613ef07f chore(deps): update gitea actions to 13d8dd5
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 4m13s
2026-08-13 15:01:13 +00:00
lyrathorpe c5b41ba6fd Merge pull request 'feat(claude): WSP local build memory, and structural terseness rules for the Soviet Engineer style' (#90) from feat/claude-wsp-build-memory-and-terseness into main
CI / flake (push) Successful in 4m23s
Reviewed-on: #90
2026-08-13 13:59:37 +01:00
Emma Thorpe 7041dfebfa fix(claude): make the Soviet Engineer style enforce terseness structurally
CI / flake (pull_request) Successful in 1m16s
CI / flake (push) Skipped
The style asked for terseness in tonal terms only, so a dry register wrapped in
headers, tables and a full status recap each turn passed its self-check while
being exactly the verbose output the style exists to prevent.

Add explicit limits: a default length ceiling, headers only for four or more
items, report the delta rather than the accumulated state, and state a caveat
once. Replace the self-check with one that tests length and form rather than
tone.
2026-08-13 13:23:58 +01:00
Emma Thorpe 4d6ad47837 docs(claude): record how to build and test core-services-cloud locally
The repo documents its own build and test commands, but assumes Windows and
PowerShell. This captures only the deltas that make them run on this machine:
dotnet from nixpkgs, artifactory credentials sourced per command because shell
state does not persist between tool calls, and a curl check that distinguishes
an auth failure from a code failure, since a rejected token surfaces as a
NuGet error that reads like a network fault.

Also records the two Docker Desktop leftovers that break the component test
environment, and the unleash registration a component test canary needs.
2026-08-13 13:23:58 +01:00
lyrathorpe f471d226e0 Merge pull request 'feat(work): headless Secret Service for gcx keychain tokens' (#89) from feat/gcx-secret-service into main
CI / flake (push) Successful in 3m58s
Reviewed-on: #89
2026-08-11 15:09:26 +01:00
Emma ThorpeandClaude Opus 5 10f713103c feat(work): headless Secret Service for gcx keychain tokens
CI / flake (pull_request) Successful in 3m57s
CI / flake (push) Skipped
gcx stores its OAuth access and refresh tokens in the system keychain
unconditionally -- its config file keeps only opaque `keychain:gcx:v2:...`
handles -- and exposes no plaintext fallback. With nothing owning
org.freedesktop.secrets on this headless WSL box, `gcx login` authenticates
against Grafana and then dies writing its config: "The name is not activatable".

Add services.headlessSecretService: gnome-keyring as a systemd --user service,
unlocking the login keyring at start. home-manager's own services.gnome-keyring
does not fit here on two counts -- it is WantedBy graphical-session-pre.target,
which never activates without a desktop session, and it passes no --unlock, so
writes would block on a GUI prompter that does not exist.

Only the secrets component is started. The ssh component is deliberately off: it
would claim SSH_AUTH_SOCK and displace services.ssh-agent, breaking SSH auth and
signed commits.

The unlock password defaults to a random one generated on first activation under
$XDG_DATA_HOME. The passwordFile option is the seam for supplying it from an
agenix secret instead, once that lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:03:18 +01:00
lyrathorpe cc6cb24c78 Merge pull request 'feat(work): install gcx on the work profile' (#88) from feat/gcx-work-profile into main
CI / flake (push) Successful in 3m58s
Reviewed-on: #88
2026-08-11 14:30:25 +01:00
Emma Thorpe 240facdbbb feat(work): install gcx on the work profile
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 3m57s
gcx is the Grafana Cloud CLI (dashboards, datasources, SLOs, synthetic
monitoring, alerts), used against the Citrix Grafana stack.

Pull it from nixpkgs-unstable via the existing overlay rather than the pinned
channel: 26.05 ships 0.2.14, which predates the stacks/contexts configuration
model and the agento11y commands, so the current tooling and docs do not apply
to it.
2026-08-11 14:24:52 +01:00
renovate-bot c1456decaf Merge pull request 'chore(deps): lock file maintenance flake inputs' (#87) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 4m22s
2026-08-10 00:07:11 +01:00
Renovate Bot e06495ae69 chore(deps): lock file maintenance flake inputs
CI / flake (pull_request) Successful in 5m7s
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
2026-08-09 23:01:49 +00:00
lyrathorpe 6868182ef5 Merge pull request 'feat(darwin): install mole' (#86) from feat/mole-macos into main
CI / flake (push) Successful in 4m19s
Reviewed-on: #86
2026-08-07 11:40:58 +01:00
lyrathorpe 90a57ab73b feat(darwin): install mole
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m24s
useful to clean up caches
2026-08-07 11:35:06 +01:00
lyrathorpe 75f4e22624 Merge pull request 'feat: add darktable to all systems' (#85) from feat/darktable-install into main
CI / flake (push) Successful in 4m8s
Reviewed-on: #85
2026-08-07 11:16:17 +01:00
lyrathorpe cf96fec63e feat: add darktable to all systems
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 3m54s
so i can edit photos wherever i have a gui
2026-08-07 11:11:55 +01:00
lyrathorpe 3cdf4d4e54 Merge pull request 'chore(claude): require ticket-scoped conventional commits on every commit' (#84) from chore/claude-memory-commit-conventions into main
CI / flake (push) Successful in 4m35s
Reviewed-on: #84
2026-08-06 16:57:27 +01:00
Emma Thorpe f61a206977 style(claude): apply prettier formatting to the git conventions memory
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 1m6s
treefmt runs prettier over markdown in this repository and the CI
formatting check failed on the two preceding commits. Prettier prefers
underscores for emphasis and requires blank lines around fenced code
blocks.

No wording changes.
2026-08-06 16:54:16 +01:00
Emma Thorpe 1d5a5adbcc chore(claude): exempt repos without an issue tracker from the ticket scope
CI / flake (push) Skipped
CI / flake (pull_request) Failing after 1m8s
The previous commit required a ticket scope on every commit in every
repository. This repository has no Jira project, so the rule as written
would either block a commit or invite a fabricated WSP number.

Record the exception: in personal repositories the scope is the area of
the change (claude, deps, hosts) and conventional form still applies.
The ticket requirement is scoped to the Jira-backed work repositories
that enforce it in CI.
2026-08-06 15:20:53 +01:00
Emma Thorpe 4029866ed4 chore(claude): require ticket-scoped conventional commits on every commit
The git conventions memory said to match the repository's existing log
style. Several repositories (multicluster, core-services-cloud) have
histories dominated by bare "WSP-1234: summary" subjects, so matching
them produced commits that were not in conventional form. A related
failure was scope decay within a session: the first commit was correct
and later ones degraded to bare "test:" or "refactor:" subjects. Both
required commit history to be rebased by hand.

- Make "<type>(<TICKET-ID>): <summary>" mandatory on every commit and
  explicitly override repository log style. Style matching now applies
  to branch names only.
- Describe how to establish the real ticket ID (named in the request,
  extracted from the branch, or taken from existing commits on the
  branch) and require asking rather than guessing when none is
  available. Replace the literal WSP-1234 examples with <TICKET-ID> so
  the placeholder cannot be committed verbatim.
- Record scope decay across a session as a named failure mode.
- Cover merge commits, preferring rebase and requiring an explicit
  message when a merge commit is unavoidable.
- Add a pre-push verification grep that must return no output.
- Note that a clean git log does not prove a subject was correct when
  written, because rebasing replaces it; compare author and committer
  dates instead.

Update the MEMORY.md index entry to match.
2026-08-06 15:20:16 +01:00
renovate-bot 66b27517ba Merge pull request 'chore(deps): lock file maintenance flake inputs' (#83) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 5m13s
2026-08-03 00:08:39 +01:00
Renovate Bot 6b43e76457 chore(deps): lock file maintenance flake inputs
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 5m50s
2026-08-02 23:02:21 +00:00
41 changed files with 1825 additions and 390 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ jobs:
# Nix drives the formatting check, so install it unconditionally.
- name: Install Nix
uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31
uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
with:
extra_nix_config: |
experimental-features = nix-command flakes
+11
View File
@@ -42,6 +42,17 @@ prettier formats `*.md`, so **documentation edits must be run through `nix fmt`*
exactly like code. prettier re-aligns Markdown tables in particular; hand-editing
a table almost always leaves it non-conformant and fails the `formatting` check.
Prose documentation lives in `docs/` and is **published** to
<https://docs.lyrapup.pet/nixfiles/> by the separate `docs-site` repo, which
clones this one at build time. Two consequences when editing docs:
- A markdown file outside `docs/` (other than the root `README.md`) is not
synced and will never appear on the site. Put new prose in `docs/`.
- Links must follow the rules in the README's "Documentation" section: absolute
Gitea URLs to source files, relative links between `docs/` pages, and
absolute `docs.lyrapup.pet` URLs from the root README into `docs/`. The site
builds non-strict, so a broken link is silent.
The CI `formatting` step runs on **every** PR — including docs- and config-only
changes — so a Markdown/YAML/JSON edit is format-checked before merge, not just
after it lands on `main`. (The heavier `deadnix`/`statix`/`pre-commit` lints and
+97 -36
View File
@@ -5,24 +5,25 @@ single flake.
## Hosts
Defined in the host table in [`flake.nix`](./flake.nix):
Defined in the host table in [`flake.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/flake.nix):
| Configuration | System | Machine |
| --------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) |
| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](./hosts/T400/README.md) |
| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](./hosts/MacPro31/README.md) |
| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](./hosts/EDaaS/README.md) |
| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](./hosts/RPi5/README.md) |
| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](./hosts/Darwin/README.md) |
| Configuration | System | Machine |
| --------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) |
| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/t400/) |
| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/macpro31/) |
| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/edaas/) |
| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/rpi5/) |
| `lyrathorpe-zero2w` | `aarch64-linux` | Raspberry Pi Zero 2 W "Psion sidecar": PPP over RS232 + legacy mail proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/pizero2w/) |
| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/darwin/) |
Shared layers: `home` (home-manager: shell, git, editor),
`modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches),
`modules/workstation.nix` (physical graphical hosts: audio, thermald,
earlyoom, fwupd), `modules/laptop.nix` (laptops: Wi-Fi, Bluetooth, power,
lid), `modules/desktop.nix` (wired desktops: NetworkManager), and
`modules/ssh.nix` (key-only sshd). The x86 hosts also pull `nixos-hardware`
profiles. The full module catalogue is below.
`modules/ssh.nix` (key-only sshd). The x86 hosts and both Raspberry Pis also
pull `nixos-hardware` profiles. The full module catalogue is below.
## Repository layout
@@ -30,7 +31,8 @@ profiles. The full module catalogue is below.
flake.nix # inputs, mkHost/mkDarwinHost, the host tables, dev shell + checks
flake.lock # pinned input revisions (Renovate keeps this fresh)
modules/ # reusable NixOS system modules (see "Module catalogue")
home/ # home-manager profile: shell, git, editor, claude, desktop, sway
home/ # home-manager profile: shell, git, editor, claude, secret-service, desktop, sway
docs/ # all prose documentation; published to docs.lyrapup.pet (see "Documentation")
users/ # identity registry + per-user home extras (see "Users")
hosts/<Name>/ # per-machine config: configuration.nix + hardware-configuration.nix
lib/ # small pure helpers (currently the Catppuccin Mocha palette)
@@ -50,22 +52,22 @@ host's table entry.
## Module catalogue
Reusable NixOS modules under [`modules/`](./modules). "Imported by" says how a
Reusable NixOS modules under [`modules/`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/modules). "Imported by" says how a
module reaches a host: **baseModules** (every NixOS host, via `flake.nix`),
**host table** (listed explicitly per host in `flake.nix`), or **transitively**
(pulled in by another module's `imports`).
| 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. |
| `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 feature-flag options (currently `features.swayDesktop.enable`) so any host can read/set them without importing the heavy implementation module. |
| `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. |
| `laptop.nix` | host table (MBP, T400) | `imports` workstation.nix, then adds the portable bits: iwd Wi-Fi, lid suspend/lock, Bluetooth + blueman. |
| `desktop.nix` | host table (Mac Pro) | `imports` workstation.nix, then swaps Wi-Fi for wired NetworkManager. Pair with `portable = false` in the host table. |
| `sway.nix` | host table (graphical hosts) | Implementation of `features.swayDesktop`: the system Sway package, the greetd/ReGreet (cage) greeter forced to Dvorak, xdg-portal, Wayland utility packages. Home-side Sway config is in `home/sway.nix`. |
| `ssh.nix` | host table (T400, Mac Pro, RPi5) | Enables sshd, opens port 22, enforces a key-only policy (no password / keyboard-interactive, no root). Authorized keys come from the registry via `users.nix`. |
| `firmware/` | referenced by MBP host config | Committed Apple peripheral firmware blobs for the Asahi MBP (see "MacBook (Asahi) firmware"). |
| 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`, **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. |
| `laptop.nix` | host table (MBP, T400) | `imports` workstation.nix, then adds the portable bits: iwd Wi-Fi, lid suspend/lock, Bluetooth + blueman. |
| `desktop.nix` | host table (Mac Pro) | `imports` workstation.nix, then swaps Wi-Fi for wired NetworkManager. Pair with `portable = false` in the host table. |
| `sway.nix` | host table (graphical hosts) | Implementation of `features.swayDesktop`: the system Sway package, the greetd/ReGreet (cage) greeter forced to Dvorak, xdg-portal, Wayland utility packages. Home-side Sway config is in `home/sway.nix`. |
| `ssh.nix` | host table (T400, Mac Pro, both Pis) | Enables sshd, opens port 22, enforces a key-only policy (no password / keyboard-interactive, no root). Authorized keys come from the registry via `users.nix`. |
| `firmware/` | referenced by MBP host config | Committed Apple peripheral firmware blobs for the Asahi MBP (see "MacBook (Asahi) firmware"). |
Form-factor decision: a **laptop** imports `laptop.nix` (default
`portable = true`); a **wired desktop** imports `desktop.nix` and sets
@@ -74,16 +76,39 @@ Form-factor decision: a **laptop** imports `laptop.nix` (default
serves. `portable` is threaded through to `home/sway.nix`, which drops the
battery block and brightness keys on desktops.
## CPU capability gating
Not every host can run everything the fleet installs. Nix cannot probe the CPU
(evaluation is pure, and a host may be built elsewhere), so each machine
declares what it is and the shared modules derive from that:
- `features.cpu.microarchLevel` — the x86-64 psABI level the CPU implements
(1 = baseline, 2 = SSE4.2/POPCNT, 3 = AVX2, 4 = AVX-512). Defaults to **2**;
only a host older than that sets it (the Mac Pro 3,1's 2008 Harpertown Xeons
are level 1). Ignored on non-x86_64 hosts.
- `features.claudeCode.enable` — derived: on unless the host is below
x86-64-v2, because Claude Code's Node runtime needs SSE4.2/POPCNT.
[`home/claude.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/claude.nix) reads it through home-manager's
`osConfig` and installs nothing (CLI, `CLAUDE.md`, output style, memory
symlink) when it is off. Hosts with no such option — the Darwin host and the
standalone `homeConfigurations` — fall back to enabled.
- An assertion in `features.nix` fails evaluation if a host force-enables a
flag its declared CPU level cannot support, so the mistake surfaces in
`nix flake check`/CI rather than as an illegal-instruction crash on the box.
Adding another CPU-sensitive tool means deriving one more flag there, not
editing every host.
## Users
Identity is data, kept separate from the reusable modules:
- [`users/registry.nix`](./users/registry.nix) — one entry per user (display
- [`users/registry.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/registry.nix) — one entry per user (display
name, email, supplementary groups, authorized + signing keys). This is the
single source of identity; no user data is hardcoded in the modules.
- Each host's table entry declares a `users` set keyed by username; every entry
lists that user's home-module composition (the shared `./home` bundle plus any
per-user modules, e.g. [`users/emmathorpe/work.nix`](./users/emmathorpe/work.nix))
per-user modules, e.g. [`users/emmathorpe/work.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/emmathorpe/work.nix))
and optional per-host-user system bits such as `linger`.
- `mkHost` builds each account from the registry and injects the matching
identity into that user's home config as the `identity` module arg. A host can
@@ -91,12 +116,13 @@ Identity is data, kept separate from the reusable modules:
Per-user home extras live under `users/<name>/`:
- [`users/lyrathorpe/home.nix`](./users/lyrathorpe/home.nix) — personal extras
- [`users/lyrathorpe/home.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/lyrathorpe/home.nix) — personal extras
(an ssh host shortcut, gammastep coordinates); imported on Lyra's hosts.
- [`users/emmathorpe/work.nix`](./users/emmathorpe/work.nix) — the work
toolchain (kubectl/helm/az/etc.), work-only LSP servers, and the corporate ssh
handling; imports
[`users/emmathorpe/renovate-review.nix`](./users/emmathorpe/renovate-review.nix),
- [`users/emmathorpe/work.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/emmathorpe/work.nix) — the work
toolchain (kubectl/helm/az/etc.), work-only LSP servers, the corporate ssh
handling, and the headless Secret Service that gcx needs for its keychain
tokens (see [`home/secret-service.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/secret-service.nix)); imports
[`users/emmathorpe/renovate-review.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/emmathorpe/renovate-review.nix),
the daily headless Renovate-PR review timer (EDaaS only).
### Portable home (off-NixOS / external consumers)
@@ -155,17 +181,20 @@ automatically.
## Shell environment & keybindings
- Interactive shell features (zsh, tmux, git, ssh, CLI tools, auto-tmux):
[`home/README.md`](./home/README.md).
[`docs/shell.md`](https://docs.lyrapup.pet/nixfiles/shell/).
- Which classic utilities are shadowed by modern replacements, and the flag
differences that will bite:
[`docs/shell.md` → "Replacing the classics"](https://docs.lyrapup.pet/nixfiles/shell/#replacing-the-classics).
- All Sway / tmux / foot / zsh keyboard shortcuts:
[`home/KEYBINDINGS.md`](./home/KEYBINDINGS.md).
[`docs/keybindings.md`](https://docs.lyrapup.pet/nixfiles/keybindings/).
## Login / greeter
Graphical (Sway) hosts log in through a Wayland greeter — `greetd` running
ReGreet inside the `cage` kiosk compositor — implemented in
[`modules/sway.nix`](./modules/sway.nix), gated on
[`modules/sway.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/modules/sway.nix), gated on
`features.swayDesktop.enable` (the option is declared in
[`modules/features.nix`](./modules/features.nix), so headless hosts
[`modules/features.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/modules/features.nix), so headless hosts
can leave it off without importing `modules/sway.nix`). The greeter is forced to Dvorak
to match the console and Sway session. Headless hosts (the WSL work box and the
Raspberry Pi server) keep plain TTY login. The target account needs a password
@@ -185,6 +214,38 @@ To refresh them, copy the firmware extracted during the Asahi install (from
[Asahi NixOS docs](https://github.com/tpwrules/nixos-apple-silicon)) into
`modules/firmware/` and commit with `git add -f`.
## Documentation
All prose documentation lives in [`docs/`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/docs); this README is the overview. The pages are
published to **<https://docs.lyrapup.pet/nixfiles/>** by the
[`docs-site`](https://code.emmathe.dev/lyrathorpe/docs-site) repository, which clones this repo on
every build (on its own push, nightly, or on demand) and assembles the tree:
```
README.md -> docs/nixfiles/index.md # this file becomes the section landing page
docs/ -> docs/nixfiles/ # everything here, ordering from docs/.pages
```
Nothing is pushed from this side and there is no build step here — editing a
page and merging is all that is required. Files outside `docs/` (bar this
README) are **not** synced, so a doc kept next to the code it describes will
never appear on the site.
### Linking rules
The site has no copy of the source tree, and this README is republished at a
different depth from the rest of `docs/`. Both facts break naive relative
links, so:
| Link from | To | Use |
| ----------------- | ----------------------- | ---------------------------------------------------------------- |
| anywhere | a source file or dir | absolute `https://code.emmathe.dev/.../src/branch/main/…` |
| a page in `docs/` | another page in `docs/` | relative (`./keybindings.md`) — correct in Gitea and on the site |
| this README | a page in `docs/` | absolute `https://docs.lyrapup.pet/nixfiles/…` |
`mkdocs build` runs non-strict on the docs-site side, so a broken link fails
silently rather than failing the build. Check links by hand when moving a page.
## Development
A dev shell and a formatting/lint gate are wired through the flake:
@@ -200,7 +261,7 @@ A dev shell and a formatting/lint gate are wired through the flake:
## CI
[`.gitea/workflows/ci.yaml`](./.gitea/workflows/ci.yaml) runs `nix flake check`
[`.gitea/workflows/ci.yaml`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/.gitea/workflows/ci.yaml) runs `nix flake check`
(formatting, `deadnix`, `statix`, the pre-commit hooks) and evaluates every
NixOS and Darwin host configuration on push/PR. It always runs (no `paths:`
filter) so the required check never hangs pending; the heavy Nix steps are
+16
View File
@@ -0,0 +1,16 @@
# Section title and ordering for the MkDocs awesome-pages plugin on
# docs.lyrapup.pet.
#
# The title is set explicitly: with no entry in the site's nav, MkDocs derives
# the section name from the directory and renders it title-cased as "Nixfiles".
title: nixfiles
# `index.md` is this repository's root README, copied in by the docs-site build
# before this directory is synced over the top. The trailing `...` picks up any
# page added later, so a new file needs no edit here.
nav:
- index.md
- shell.md
- keybindings.md
- hosts
- ...
+1
View File
@@ -0,0 +1 @@
title: Hosts
+36 -2
View File
@@ -11,7 +11,7 @@ The day-to-day work environment. It layers the corporate Kubernetes / Helm /
Terraform / cloud toolchain and a couple of work-only editor language servers on
top of the shared home profile. The system config here is thin — it is mostly
WSL plumbing; the user-facing tooling lives in
[`../../users/emmathorpe/work.nix`](../../users/emmathorpe/work.nix).
[`../../users/emmathorpe/work.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/emmathorpe/work.nix).
## WSL specifics
@@ -34,10 +34,44 @@ WSL plumbing; the user-facing tooling lives in
The host-table entry sets `users.emmathorpe.linger = true` so the user's
`systemd --user` instance stays alive without an open login session. That keeps
the daily headless **Renovate PR review** timer firing — defined in
[`../../users/emmathorpe/renovate-review.nix`](../../users/emmathorpe/renovate-review.nix)
[`../../users/emmathorpe/renovate-review.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/emmathorpe/renovate-review.nix)
(imported only from `work.nix`, so it exists on this machine alone). See that
file's header for the auth (Vertex AI ADC), triage policy, and caveats.
## Secret Service (keychain)
`work.nix` sets `services.headlessSecretService.enable = true`, which runs
`gnome-keyring` as a `systemd --user` service owning `org.freedesktop.secrets`
on the session bus, with the login keyring unlocked at start.
This exists for **gcx**, the Grafana Cloud CLI. gcx stores its OAuth access and
refresh tokens in the keychain unconditionally (its config keeps only opaque
`keychain:gcx:v2:...` handles) and has no plaintext fallback, so without a
Secret Service `gcx login` authenticates and then fails to persist with "The
name is not activatable".
Home-manager's own `services.gnome-keyring` does not work here: it is
`WantedBy=graphical-session-pre.target`, which never activates on this headless
box, and it cannot unlock the keyring. See
[`../../home/secret-service.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/secret-service.nix) for the full
rationale and the security trade-off of an auto-unlocked keyring.
Only the `secrets` component is started. The `ssh` component is deliberately off
— it would claim `SSH_AUTH_SOCK` and displace `services.ssh-agent`, breaking SSH
auth and signed commits.
Checking it:
```sh
systemctl --user status headless-secret-service
busctl --user list | grep secrets # expect org.freedesktop.secrets
secret-tool search --all service gcx # inspect what gcx stored
gcx config check # end-to-end
```
If the keyring password is ever lost or changed, the login keyring cannot be
unlocked: delete `~/.local/share/keyrings` and re-run `gcx login`.
## stateVersion
`system.stateVersion = "24.11"` — the release this box was first installed on.
+138
View File
@@ -0,0 +1,138 @@
# Mac Pro 3,1 (Early 2008) — install notes
Flake host: `lyrathorpe-macpro31`. Desktop (`portable = false`, imports
`../../modules/desktop.nix`). Files: `configuration.nix`, `nvidia.nix`,
`hardware-configuration.nix`.
## Hardware configuration
`hardware-configuration.nix` here is the real config generated by
`nixos-generate-config` on the machine. Root is an **LVM** logical volume
(`/dev/mapper/MacPro-Root`, ext4); the ESP (vfat) and swap are referenced by
UUID. The initrd carries `dm-snapshot` for the LVM root. Regenerate and commit
if the disk layout changes.
## Bootloader
The Mac Pro 3,1 has **64-bit EFI**, so it uses **systemd-boot** (no GRUB/CSM
shim). `canTouchEfiVariables = false` because Apple's firmware does not reliably
accept `efibootmgr` NVRAM writes.
Apple-EFI quirk: if the firmware boot picker does not show NixOS after install,
either
- uncomment `boot.loader.efi.efiInstallAsRemovable = true;` in
`configuration.nix` (installs the fallback `\EFI\BOOT\BOOTX64.EFI`), and/or
- "bless" the ESP from macOS.
Partition the disk GPT with an ESP (vfat).
## Graphics — NVIDIA Quadro P400
The stock card (**ATI Radeon HD 2600 XT** or **NVIDIA GeForce 8800 GT**,
depending on the unit) has been replaced with an **NVIDIA Quadro P400** (Pascal,
GP108). Everything driver-related lives in [`nvidia.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/hosts/MacPro31/nvidia.nix):
- **Driver branch 580** (`nvidiaPackages.legacy_580`), _not_ the nixpkgs default
(`production`, currently 595.x). 580 is the last branch that supports
Maxwell/Pascal/Volta and is maintained as an LTS branch until Aug 2028; a
newer branch does not drive this card at all.
- `modesetting.enable = true` — mandatory for Wayland (sets
`nvidia-drm.modeset=1`); without it wlroots gets no GBM device and both Sway
and the greeter fail to start.
- `open = false` — the open kernel modules require Turing or later.
- Sway runs with `--unsupported-gpu` (`programs.sway.extraOptions`); wlroots
refuses the proprietary driver otherwise. `cage`/ReGreet needs no such flag.
- nouveau and `nvidiafb` are blacklisted automatically by the NVIDIA module.
The driver is unfree, so it is **not in the binary cache**: the kernel module is
compiled on the machine, which on these 2008 Xeons is slow — budget for a long
first rebuild and again after every kernel bump. The package names are
allowlisted in `unfreePackages` in [`flake.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/flake.nix).
Note the Mac Pro shows no EFI boot screen with a stock PC card (no Apple EFI
ROM): the machine boots blind until KMS brings the display up. That is expected,
not a fault.
Verify after a rebuild:
```sh
nvidia-smi
```
## Docker with CUDA
`nvidia.nix` also enables Docker and gives containers GPU access via **CDI**
(`hardware.nvidia-container-toolkit.enable`), which generates device specs from
the host driver at boot (regenerated by a udev rule when the `nvidia` device
appears) and turns on the daemon's CDI feature:
```sh
docker run --rm --device=nvidia.com/gpu=all nvidia/cuda:12.9.1-base-ubuntu24.04 nvidia-smi
```
- Use the `--device=nvidia.com/gpu=all` form. `--gpus all` is the legacy
runtime-wrapper path (`virtualisation.docker.enableNvidia`), which is
deprecated upstream and deliberately not enabled here.
- **CUDA version matters.** The P400 is compute capability 6.1 (`sm_61`); CUDA
13 dropped Maxwell/Pascal/Volta, so container images must ship a **CUDA 12.x
or older** runtime. The 580 driver itself is happy with either.
- 2 GB of VRAM, 256 CUDA cores — fine for encode/decode and small models, not
for training anything serious.
- Docker socket is local-only (no TCP listener, unlike the Pi). Users need the
`docker` group; the registry already grants it.
### "Driver Not Loaded" from the CDI generator
`nvidia-container-toolkit-cdi-generator.service` fails with
`failed to initialize NVML: Driver Not Loaded` whenever the `nvidia` kernel
module is not loaded in the **running** kernel. After a kernel bump that is
unavoidable — the rebuilt module cannot load until reboot — so the unit is
guarded with `ConditionPathExists=/proc/driver/nvidia/version` and skips
instead of failing. Without that guard it also takes `docker.service`
(`requiredBy`) with it and makes `nixos-rebuild switch` exit non-zero.
**Reboot after a rebuild that touches the driver or the kernel.** The toolkit's
udev rule restarts the generator when the GPU device appears, so the CDI specs
are written on the next boot. To check the state:
```sh
lsmod | grep nvidia # nvidia, nvidia_modeset, nvidia_drm, nvidia_uvm
cat /proc/driver/nvidia/version
nvidia-smi
systemctl status nvidia-container-toolkit-cdi-generator.service
ls /var/run/cdi # the generated spec
```
If the module is genuinely absent after a reboot, check `dmesg | grep -i
nvidia` (build/version mismatch, or nouveau still bound — the module blacklists
it, so that should not happen).
## Claude Code — not installed here
The dual Harpertown Xeons are **x86-64-v1** (SSE4.1, but no SSE4.2/POPCNT) and
the Node runtime Claude Code ships on requires x86-64-v2. `configuration.nix`
declares `features.cpu.microarchLevel = 1`, which switches the tool off through
the fleet-wide gate in [`../../modules/features.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/modules/features.nix)
— see the root README. Forcing `features.claudeCode.enable` on here is an
evaluation error, not a broken install.
## Networking
Wired Ethernet via NetworkManager (from `desktop.nix`) — the Mac Pro has two
gigabit ports.
## Login
Graphical login via a Wayland greeter — `greetd` running ReGreet inside the
`cage` kiosk compositor — configured centrally in `../../modules/sway.nix` for
every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced
to the Dvorak layout to match the console and Sway session. Set the user
password (`passwd lyrathorpe`) after install, or the greeter cannot
authenticate. Requires working KMS (NVIDIA modesetting — see Graphics).
## Apply
```sh
sudo nixos-rebuild switch --flake .#lyrathorpe-macpro31
```
+205
View File
@@ -0,0 +1,205 @@
# Raspberry Pi Zero 2 W (`lyrathorpe-zero2w`)
Headless `aarch64-linux` "Psion sidecar": an RS232 companion for a Psion 5MX,
after [Kian Ryan's PPP modem and terminal
write-up](https://www.kianryan.co.uk/2022-11-28-psion-sidecar-ppp-modem-and-terminal/).
Two roles, split into submodules:
- **PPP link + telnet** (`serial-ppp.nix`) — `pppd` on `/dev/ttyAMA0`, the Psion
on the far end of a null-modem cable, NAT out to Wi-Fi, and a telnet login for
the Psion's terminal client.
- **Legacy mail proxy** (`email-proxy.nix`) — cleartext POP3/SMTP for the
Psion's built-in mail client, forwarded to authenticated IMAPS/SMTPS by
[legacy-email-proxy](https://code.emmathe.dev/lyrathorpe/legacy-email-proxy).
That project ships its own package and NixOS module, so `email-proxy.nix`
here is only `services.legacy-email-proxy.enable` plus a path to the
credentials — nothing about the proxy is vendored into this flake.
`sd-image.nix` in the same directory is not part of the running system: it is
the one-shot install card, built as `packages.aarch64-linux.zero2w-sd-image`.
See "Install".
## Hardware and boot
The Zero 2 W is a BCM2837 — the Pi 3's SoC — so the host table uses
`nixos-hardware`'s `raspberry-pi-3` profile for the kernel, firmware and device
tree. Boot is the same U-Boot + extlinux path as the other Pi.
Unlike the Pi 5, this host owns the firmware partition declaratively
(`hardware.raspberry-pi.firmware.enable`): every `switch` rewrites
`/boot/firmware`, including `config.txt`. Two settings there matter:
| `config.txt` | Why |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dtoverlay=disable-bt` | Moves the PL011 UART off Bluetooth onto GPIO 14/15, so `/dev/ttyAMA0` is the RS232 header. The mini UART (`ttyS0`) drifts at 115200. |
| `dtoverlay=uart0,ctsrts` | RTS/CTS on GPIO 16/17. Both `pppd` and the Psion's modem profile use hardware flow control. |
| `kernel=u-boot.bin` | `hardware.raspberry-pi.firmware.uboot.enable`. Without it the rewritten `config.txt` would have no `kernel=` line and the board would stop booting. |
`gpu_mem=16`, `start_x=0`, `camera_auto_detect=0` and `display_auto_detect=0`
hand the VideoCore the minimum: the board has 512 MB total and no display.
## Never build on the Pi
512 MB of RAM and an SD card. It cannot compile its own system, and there is
deliberately no swap partition (SD cards wear out under swap writes) — zram
takes its place. Build somewhere else and push the result:
```sh
# from a workstation, using another aarch64 machine as the builder
nixos-rebuild switch --flake .#lyrathorpe-zero2w \
--build-host lyrathorpe@lyrathorpe-rpi5 \
--target-host lyrathorpe@<pi-address> --use-remote-sudo
```
The `raspberry-pi-3` profile builds the vendor kernel from source and it is not
in the binary cache, so the first build is long (hours on the Pi 5, less on the
MacBook). Later builds reuse it. The same applies to the SD image below: it
contains that kernel, so it needs an `aarch64-linux` builder too. From an
`x86_64` box or a Mac, that means a remote builder (`nix.buildMachines`) or, on
Darwin, `nix.linux-builder.enable`.
## Install
The card is built from this flake, not downloaded. A generic NixOS image would
boot, but there would be no way into the machine afterwards: it has no Ethernet,
no wifi credentials, and this configuration hands the serial port to `pppd`, so
there is no console either. Building the host's own image sidesteps all three —
the first boot is already the real system, with the SSH key from the registry
in place.
1. **Set the SSID.** `networking.wireless.networks` in `configuration.nix` still
says `CHANGE-ME-SSID`. It is baked into the image at build time; only the PSK
is read at runtime.
2. **Build and write the card.** On an `aarch64-linux` machine (or with one
configured as a builder):
```sh
nix build .#packages.aarch64-linux.zero2w-sd-image
sudo dd if=result/sd-image/nixos-zero2w.img of=/dev/sdX bs=4M conv=fsync status=progress
```
Check `/dev/sdX` twice. `dd` does not ask.
3. **Seed the secrets before first boot.** They are not in the image. Mount the
card's second partition (the ext4 root) and write both files described under
"Secrets" below:
```sh
sudo mount /dev/sdX2 /mnt
sudo mkdir -p /mnt/var/lib/wpa_supplicant /mnt/var/lib/legacy-email-proxy
printf 'psk_home=%s\n' 'the-pre-shared-key' \
| sudo tee /mnt/var/lib/wpa_supplicant/secrets.conf > /dev/null
sudo chmod 600 /mnt/var/lib/wpa_supplicant/secrets.conf
# ... and /mnt/var/lib/legacy-email-proxy/backend.env, same permissions
sudo umount /mnt
```
Skip the PSK and the board boots with no network at all.
4. **Boot it.** Give it a few minutes on first boot — it resizes the root
partition and generates host keys on a slow card. Then:
```sh
ssh lyrathorpe@lyrathorpe-zero2w.local # mDNS; services.avahi publishes it
```
5. **Give the login user a password** (`passwd lyrathorpe`) if you want console
or telnet login; the SSH key from
[`users/registry.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/registry.nix)
already works without one.
6. Thereafter, rebuild from another machine as in the previous section.
`hosts/PiZero2W/hardware-configuration.nix` is a **placeholder** — but its
layout (`/` on label `NIXOS_SD`, `/boot/firmware` on label `FIRMWARE`) is
exactly what the SD image produces, so there is nothing to regenerate for a card
install. Run `nixos-generate-config` and replace it only if you deviate from
that layout.
If the board never appears on the network, it is almost always the PSK file.
Re-mount the card and check it. Failing that, a mini-HDMI monitor and a
micro-USB keyboard get you a console on `tty1` — the serial port will not,
because `pppd` holds it.
## Secrets (not in the Nix store)
Both files are created on the device, owned by root, mode `0600`. Neither is
managed by this flake; the units that read them fail loudly if they are absent.
**Wi-Fi PSK** — `/var/lib/wpa_supplicant/secrets.conf`:
```
psk_home=<the pre-shared key>
```
The SSID itself _is_ in `configuration.nix` and is currently the placeholder
`CHANGE-ME-SSID`; set it to the real network. `wpa_supplicant` resolves
`pskRaw = "ext:psk_home"` against this file at runtime.
**Mail backend** — `/var/lib/legacy-email-proxy/backend.env`, a systemd
`EnvironmentFile`:
```
BACKEND_IMAP_HOST=imap.example.com
BACKEND_IMAP_USER=someone@example.com
BACKEND_IMAP_PASS=<app password>
BACKEND_SMTP_HOST=smtp.example.com
BACKEND_SMTP_USER=someone@example.com
BACKEND_SMTP_PASS=<app password>
```
Ports and TLS default sensibly (IMAPS 993, SMTPS 465); the full variable list is
in the proxy's README.
### Why POP3 and not IMAP
The Psion's built-in mail client speaks POP only, so POP3 is what the proxy
exposes. If a third-party IMAP client is ever installed on the device, the
answer is **not** to add an IMAP frontend to the proxy: the backend is already
IMAP, so there is no protocol to translate, only TLS to remove. An `stunnel`
client (plaintext 143 on the PPP link, IMAPS 993 outbound) does that in a few
lines with no code, and credentials pass straight through — IMAP clients always
authenticate.
SMTP stays on the proxy either way. A client of this vintage cannot do SMTP
AUTH, which is exactly why the proxy injects the backend credentials.
## Psion configuration
Matches the addressing in `serial-ppp.nix` (`10.0.0.1` the Pi, `10.0.0.2` the
Psion):
- **Modem** control panel, a "Direct Cable Connection" profile: 115200 baud,
Hardware (RTS/CTS) flow control; on the Advanced tab, Terminal Detect and
Carrier Detect both **off**.
- **Internet** control panel, a new profile: Connection Type **Direct**, Manual
Login **True**. Addresses: get IP from server **False**, static **10.0.0.2**.
Get DNS from server **True** — `pppd` sends resolvers over the link
(`ms-dns`), so nothing is hard-coded on the Psion.
- Advanced: PPP extensions **False**, plain-text authentication **True**.
- Terminal client: telnet to **10.0.0.1 port 23**. It renders non-ANSI output
far better than the raw serial console does.
- Mail client: POP3 and SMTP server **10.0.0.1**, no encryption, no
authentication.
## Security
Everything on this host that the Psion talks to is unauthenticated and
unencrypted, because a 1999 palmtop speaks no TLS:
- **telnet on 23** — cleartext login, including the password.
- **POP3 on 110 / SMTP on 25** — full mailbox access and an open relay to anyone
who reaches them.
The confinement is the firewall, and it is the only thing standing there:
`ppp0` is a trusted interface, `wlan0` is not, and those ports are never opened
on it. The proxy binds `0.0.0.0` rather than `10.0.0.1` on purpose — the PPP
address only exists while the Psion is plugged in, and a bind-time dependency on
a serial cable is a restart loop waiting to happen. Do not add these ports to
`networking.firewall.allowedTCPPorts`, and do not put this board on an untrusted
network.
Only sshd (port 22, key-only, via
[`modules/ssh.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/modules/ssh.nix))
is reachable over Wi-Fi.
## Troubleshooting
| Symptom | Check |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No PPP at all | `systemctl status pppd-psion`, then `journalctl -u pppd-psion -f` while the Psion dials. `passive`/`persist` mean it waits, not fails. |
| PPP negotiates, then hangs | Flow control. Confirm `dtoverlay=uart0,ctsrts` is in `/boot/firmware/config.txt` and that the Psion's modem profile is set to Hardware. |
| `/dev/ttyAMA0` missing or is a Bluetooth device | `disable-bt` did not apply — the firmware partition was not rewritten. Confirm `/boot/firmware` is a mounted partition; the activation script skips with a warning if it is not. |
| Something else holds the port | `systemctl status serial-getty@ttyAMA0` — it is disabled in `serial-ppp.nix`, and must stay that way. |
| Mail proxy dead | `systemctl status legacy-email-proxy`. A missing `backend.env` fails the unit before it starts. |
+7 -7
View File
@@ -4,13 +4,13 @@ Every keyboard shortcut configured across this desktop, and where it is defined.
Everything here is managed declaratively through Nix — edit the listed file and
rebuild, never the generated dotfiles.
| Area | Defined in |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| Sway (compositor) | [`sway.nix`](./sway.nix) `config.keybindings` + `config.modes`, plus the home-manager Sway module's built-in defaults |
| tmux | [`shell.nix`](./shell.nix) `programs.tmux` |
| zsh line editor | [`shell.nix`](./shell.nix) `programs.zsh.historySubstringSearch` |
| Neovim | [`editor.nix`](./editor.nix) `programs.nixvim` |
| foot (terminal) | foot package defaults — only colours are themed (in `sway.nix`) |
| Area | Defined in |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Sway (compositor) | [`sway.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/sway.nix) `config.keybindings` + `config.modes`, plus the home-manager Sway module's built-in defaults |
| tmux | [`shell.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/shell.nix) `programs.tmux` |
| zsh line editor | [`shell.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/shell.nix) `programs.zsh.historySubstringSearch` |
| Neovim | [`editor.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/home/editor.nix) `programs.nixvim` |
| foot (terminal) | foot package defaults — only colours are themed (in `sway.nix`) |
**Conventions**
+360
View File
@@ -0,0 +1,360 @@
# 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 <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 `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 <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`](./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 <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`](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 | 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 |
Generated
+61 -40
View File
@@ -3,16 +3,16 @@
"brew-src": {
"flake": false,
"locked": {
"lastModified": 1784558651,
"narHash": "sha256-woXJ1ATKpSYRWCy46TQJjmm9XzAeZVEZw9xDfVG9NYI=",
"lastModified": 1786348930,
"narHash": "sha256-bCQJkbgsAMDp5HQystZLCq11UHiyEuoWbxKulAPYrh8=",
"owner": "Homebrew",
"repo": "brew",
"rev": "b48c7994b5f0eed7bef532efa63cb4e4f763887a",
"rev": "3ecc9eff23feebf1bc73846d74e14a122c93b66f",
"type": "github"
},
"original": {
"owner": "Homebrew",
"ref": "6.0.12",
"ref": "6.0.16",
"repo": "brew",
"type": "github"
}
@@ -25,11 +25,11 @@
},
"locked": {
"dir": "pkgs/firefox-addons",
"lastModified": 1785038882,
"narHash": "sha256-Y5V6bOp5VQjoll+TeFmUki00cSnWAD88wOr/N0c+VNE=",
"lastModified": 1786853140,
"narHash": "sha256-O880FlUav75Q5aNlg9znyg/avf1X/W7o/cAtZFLtpWc=",
"owner": "rycee",
"repo": "nur-expressions",
"rev": "f352fe54c0d939110314aebfd6b442417a42add7",
"rev": "ba9568c9c0df6290dc2f34b032ab4cb575e73788",
"type": "gitlab"
},
"original": {
@@ -93,11 +93,11 @@
]
},
"locked": {
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"lastModified": 1785627969,
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
"type": "github"
},
"original": {
@@ -114,11 +114,11 @@
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"lastModified": 1785627969,
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
"type": "github"
},
"original": {
@@ -155,11 +155,11 @@
]
},
"locked": {
"lastModified": 1785119570,
"narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=",
"lastModified": 1786924861,
"narHash": "sha256-hftabkb+73OcGzvwFAjCiQorAhprs9TnU1+FkGO5CIw=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d",
"rev": "09ae1b85a6db412d841d60f924b23f881f0d0a38",
"type": "github"
},
"original": {
@@ -185,6 +185,26 @@
"type": "github"
}
},
"legacy-email-proxy": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1787315211,
"narHash": "sha256-FuZ9nXMRtnMPO/wbjYkpsKn6K/FFc64P5XDmCyfyxGs=",
"ref": "refs/heads/main",
"rev": "f1e1373fd350fd77f1848eddfa67ed9e00724c25",
"revCount": 13,
"type": "git",
"url": "https://code.emmathe.dev/lyrathorpe/legacy-email-proxy"
},
"original": {
"type": "git",
"url": "https://code.emmathe.dev/lyrathorpe/legacy-email-proxy"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
@@ -211,11 +231,11 @@
"brew-src": "brew-src"
},
"locked": {
"lastModified": 1784906761,
"narHash": "sha256-2v0H8+Ert+xbm0oliHblXTPPczuKiibBUBvRax59kxg=",
"lastModified": 1786686423,
"narHash": "sha256-8q3WdB8o3VUI7rOz1OXfioXIaaWbFTAxRJAkWLlfc0s=",
"owner": "zhaofengli",
"repo": "nix-homebrew",
"rev": "60623ec512406261f553d24033c8a0c53fd0b7f2",
"rev": "ccabf79a6b9845eb72b51ea1d9c7ce3446350df3",
"type": "github"
},
"original": {
@@ -231,11 +251,11 @@
]
},
"locked": {
"lastModified": 1785046085,
"narHash": "sha256-UiK+mmZJuLWQVhJ5b2wDzogIYWAesyRm6LA3h3Ulh3Y=",
"lastModified": 1786852476,
"narHash": "sha256-IM5CYtf86W4w8eUPpKcY/LpdHElmVBtJhaKnoTKxZEA=",
"owner": "nix-community",
"repo": "nix-index-database",
"rev": "11665045df8b9938ef811a3bfdc65cffb02b4b70",
"rev": "c7962dc97b45129df8d751bedaf37beb5a17706e",
"type": "github"
},
"original": {
@@ -252,11 +272,11 @@
]
},
"locked": {
"lastModified": 1784496018,
"narHash": "sha256-wmEC6UPwmBCILILU8iCdzdd74P1eA8P5gFGYnptA4qU=",
"lastModified": 1786862401,
"narHash": "sha256-zRPYCn5RJWxr9uyUwNIQjPsTFcIFRwuRnI91dqvGA0k=",
"owner": "nix-community",
"repo": "nixos-apple-silicon",
"rev": "3902c801519264191a7c3dfec8dd1f9faeb38fd5",
"rev": "53798a0eb0fa4c8cfaeca7bdc5b4ad22ed210c95",
"type": "github"
},
"original": {
@@ -272,11 +292,11 @@
]
},
"locked": {
"lastModified": 1784723954,
"narHash": "sha256-1CfD8ZUjCkTgjsneLZ/lxCHhgDfqxxE7/GX0MmsgiqA=",
"lastModified": 1786867632,
"narHash": "sha256-ez+ubZlA1RtdjCB18a6zJ9M4u8qoPDy08EcnsW5M3Xw=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "a017f5b72210026af5b3ac5949f08d94380a6fbd",
"rev": "ff17823245ab9ff7bcae6acf950bd89cba82c38c",
"type": "github"
},
"original": {
@@ -308,11 +328,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1785002762,
"narHash": "sha256-Y0BbgB3BLLHEUK88g4oSp3DerIx7rCX0iwICKJcY7/c=",
"lastModified": 1786711500,
"narHash": "sha256-QvnceIGTBeDvDd9oCn+GvdsnkquliuwbVgpiRH68qaQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f7a2e428f5d71c47a5a938a3c5ad7138bb291093",
"rev": "02e08985a27c65ffd33d434eeb2e660a2e4dc84d",
"type": "github"
},
"original": {
@@ -324,11 +344,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1785090369,
"narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=",
"lastModified": 1786862985,
"narHash": "sha256-FBJRXmbGXiSUDvYEbfLYRkckayyZ6SK1UEqhCrIZ2Cs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "624af665418d3c65d544145b4d34ad696439570e",
"rev": "e5bdc4a41d4c072fe1e3787eaa0320a384741d44",
"type": "github"
},
"original": {
@@ -347,11 +367,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1782919967,
"narHash": "sha256-pRwjfB5HQJ3m8J8bOR43pPHtHI7VUJSqwLA3P06cOY0=",
"lastModified": 1786873773,
"narHash": "sha256-Hj/nkhKDv0aJly1PAUstrhrgEYn1mVSkLIYMh90r/Pc=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "667c8471f4a0fb24d702d1a61af8609f1a5f1ba6",
"rev": "b397fb9f6950d57355d62bb92457d223464e0115",
"type": "github"
},
"original": {
@@ -368,6 +388,7 @@
"git-hooks": "git-hooks",
"home-manager": "home-manager",
"kube-tmux": "kube-tmux",
"legacy-email-proxy": "legacy-email-proxy",
"nix-darwin": "nix-darwin",
"nix-homebrew": "nix-homebrew",
"nix-index-database": "nix-index-database",
@@ -402,11 +423,11 @@
]
},
"locked": {
"lastModified": 1784369104,
"narHash": "sha256-47cxbcZODibHv3rELFQ9vZly0vUNkND/atn/U7HLeb0=",
"lastModified": 1786901030,
"narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "df3c0640565d04a0261253cdd89fce78ec50168a",
"rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa",
"type": "github"
},
"original": {
+55 -2
View File
@@ -67,6 +67,13 @@
url = "github:jonmosco/kube-tmux";
flake = false;
};
# legacy-email-proxy: cleartext POP3/SMTP front end for the Psion's mail
# client, proxied to authenticated IMAPS/SMTPS. Ships its own package and
# NixOS module; the Pi Zero 2 W host just enables the service.
legacy-email-proxy = {
url = "git+https://code.emmathe.dev/lyrathorpe/legacy-email-proxy";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
@@ -84,7 +91,9 @@
flake-parts.lib.mkFlake { inherit inputs; } (
{ lib, ... }:
let
# claude-code tracks nixpkgs-unstable regardless of the pinned nixpkgs.
# These track nixpkgs-unstable regardless of the pinned nixpkgs.
# gcx: 26.05 ships 0.2.14, which predates the stacks/contexts config
# model and the agento11y commands the tooling expects.
overlays = [
(_final: prev: {
inherit
@@ -93,6 +102,7 @@
config.allowUnfree = true;
})
claude-code
gcx
;
})
# commitizen 4.13.9's regression test for the invalid-command error
@@ -108,8 +118,14 @@
];
# Unfree packages permitted to be built (replaces blanket allowUnfree).
# The NVIDIA entries are for the Mac Pro's Quadro P400 (hosts/MacPro31/
# nvidia.nix); unfree packages are not in the binary cache, so the
# kernel module is compiled on the host.
unfreePackages = [
"claude-code"
"nvidia-x11"
"nvidia-kernel-modules"
"nvidia-settings"
];
# Per-user identity, keyed by username. See README "Users".
@@ -318,6 +334,26 @@
./users/lyrathorpe/home.nix
];
};
lyrathorpe-zero2w = {
system = "aarch64-linux";
portable = false;
# Headless "Psion sidecar": PPP over RS232 plus a legacy mail proxy
# (hosts/PiZero2W/). No sway.nix; the raspberry-pi-3 profile carries
# the kernel/firmware/device tree (the Zero 2 W is the Pi 3's
# BCM2837 SoC) and ssh.nix adds key-only sshd. This board has 512 MB
# of RAM and never builds its own system -- see
# docs/hosts/pizero2w.md.
modules = [
./hosts/PiZero2W/configuration.nix
inputs.nixos-hardware.nixosModules.raspberry-pi-3
./modules/ssh.nix
];
users.lyrathorpe.homeModules = [
./home
./users/lyrathorpe/home.nix
];
};
};
# Darwin host table — macOS machines built via mkDarwinHost. The shared
@@ -356,8 +392,24 @@
# nixpkgs instance for that system. Outputs here become per-system
# attrsets automatically (e.g. devShells.<system>.default).
perSystem =
{ config, pkgs, ... }:
{
config,
pkgs,
system,
...
}:
{
# One-shot SD card for bringing the Pi Zero 2 W up: that host's own
# configuration plus the sd-image module, so the first boot is
# already the real system. aarch64-linux only -- building it needs
# an aarch64 Linux builder. See docs/hosts/pizero2w.md.
packages = lib.optionalAttrs (system == "aarch64-linux") {
zero2w-sd-image =
((mkHost hosts.lyrathorpe-zero2w).extendModules {
modules = [ ./hosts/PiZero2W/sd-image.nix ];
}).config.system.build.sdImage;
};
# treefmt drives `nix fmt` and the formatting check below. nixfmt
# stays the .nix formatter (the tree is already nixfmt-formatted);
# shfmt covers shell and prettier covers markdown/yaml/json.
@@ -432,6 +484,7 @@
git = ./home/git.nix;
editor = ./home/editor.nix;
claude = ./home/claude.nix;
secret-service = ./home/secret-service.nix;
desktop = ./home/desktop.nix;
sway = ./home/sway.nix;
};
-215
View File
@@ -1,215 +0,0 @@
# 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`](./shell.nix) |
| git (+ delta, commitizen) | [`git.nix`](./git.nix) |
| Neovim (nixvim) + LSP | [`editor.nix`](./editor.nix) |
| Claude Code (CLAUDE.md, style, memory) | [`claude.nix`](./claude.nix) |
| GUI apps, GTK/Firefox theming, cursor | [`desktop.nix`](./desktop.nix) (graphical hosts only) |
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,
signing key) comes from the user registry
([`../users/registry.nix`](../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`. 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 |
**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`).
## 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`](./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 <sub>` (e.g. `git cz c`) and `git cc` → commitizen prompt |
| 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`](./claude.nix) on every host (the CLI is
`pkgs.claude-code`, tracked to unstable via the flake overlay).
| 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/`](./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) |
| 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 |
+21 -5
View File
@@ -1,4 +1,5 @@
# Claude Code, configured declaratively via home-manager. Wanted on every host.
# Claude Code, configured declaratively via home-manager. Wanted on every host
# whose CPU can run it -- see the gate below.
#
# The STATIC config is managed here: the global CLAUDE.md (persona/context), the
# custom output style, and the auto-memory directory. settings.json is
@@ -10,18 +11,33 @@
# read-only into ~/.claude/memory, so the runtime "save a memory" path no longer
# writes there -- recall still works, but new/changed memories must be added to
# this repo and rebuilt. CLAUDE.md instructs Claude to do exactly that.
{ ... }:
{
lib,
# Set by the NixOS/Darwin home-manager module; absent for the standalone
# homeConfigurations, hence the default.
osConfig ? { },
...
}:
let
# Capability gate, declared once for the whole fleet in modules/features.nix
# (default: on; off on CPUs below x86-64-v2, which cannot run the Node
# runtime Claude Code ships on). Hosts without that option -- the Darwin host
# and the portable standalone profile -- fall back to enabled.
enable = osConfig.features.claudeCode.enable or true;
in
{
programs.claude-code = {
enable = true;
inherit enable;
# package defaults to pkgs.claude-code (tracked to unstable via the flake
# overlay); installs the CLI on every host.
# overlay).
# ~/.claude/CLAUDE.md -- global instructions / persona / memory workflow.
context = ./claude/CLAUDE.md;
};
home.file = {
# Nothing to place when the CLI is not installed: a ~/.claude/memory symlink
# with no Claude Code to read it is just dead state.
home.file = lib.mkIf enable {
# Custom output style. The module has no option for output-styles/, so place
# it directly; selection (settings.json `outputStyle`) stays mutable.
".claude/output-styles/soviet-engineer.md".source = ./claude/output-styles/soviet-engineer.md;
+2 -1
View File
@@ -1,6 +1,6 @@
- [User name](user_name.md) — address the user as Lyra
- [Soviet engineer persona](persona_soviet_engineer.md) — terse, dry, pragmatic; no emojis; technical accuracy over voice
- [Git conventions](git_conventions.md) — never commit to main, always a branch; Conventional Commits branches and messages; inspect repo style first; commit at logical checkpoints
- [Git conventions](git_conventions.md) — never commit to main, always a branch; EVERY commit is `type(<TICKET-ID>): summary` using the live ticket, overrides repo's bare-prefix style; watch for scope decay on follow-up commits; grep to verify before pushing
- [Git network ops](git_network_ops.md) — GitHub and Gitea (code.emmathe.dev) both pushable in-sandbox (sandbox off, agent key); raise Gitea PRs via tea CLI
- [Git commit signing](git_commit_signing.md) — signs in-sandbox via ssh-agent (allowAllUnixSockets + inlined pubkey); sig=N without allowedSignersFile is cosmetic, still signed
- [Git check state first](git_check_state.md) — always check branch/status/divergence before git work; Lyra edits repos between sessions
@@ -14,3 +14,4 @@
- [Sandbox prompts](feedback_sandbox_prompts.md) — don't prompt for sandbox-disable or routine read-only shell ops; broaden permissions instead
- [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed
- [Nix shell tooling](nix_shell_tooling.md) — any nixpkgs tool runs ad hoc via `nix run`/`nix shell nixpkgs#<pkg>`; a missing command is never a dead end
- [WSP local build and test](wsp_local_build_and_test.md) — core-services-cloud on this box: dotnet via nix, artifactory creds from `~/.artifactoryenv` sourced per command, how to tell auth failure from a code failure
+29 -3
View File
@@ -11,8 +11,34 @@ metadata:
**Branch naming:** Follow the repo's existing convention — inspect with `git branch -a` or `git for-each-ref` before creating. Prefer Conventional Commits prefixes (`feat/`, `fix/`, `chore/`, `docs/`, `refactor/`). Format: `<prefix>/<TICKET-ID>-<kebab-summary>`. Only ask if no convention is discoverable.
**Commit messages:** Conventional Commits. Subject line: `<type>(<TICKET-ID>): <imperative summary>` ticket ID as the scope. Use additional `-m` flags for rationale/body. Commit at logical checkpoints, not one giant final commit.
**Commit messages — every commit, without exception:** `<type>(<TICKET-ID>): <imperative summary>`. The ticket ID goes in the scope. Use additional `-m` flags for rationale/body. Commit at logical checkpoints, not one giant final commit.
**Why:** Lyra's standard workflow for traceability and clean history.
**`<TICKET-ID>` is the real ticket for the work in hand.** It is a symbol to substitute, never a literal — if a commit subject ever reaches git still containing `<TICKET-ID>`, or a made-up number, that is a defect. Establish the actual ID before the first commit, in this order:
**How to apply:** Whenever creating a branch or committing in any repo. Inspect existing branches/log first so you match the repo's actual style; the format above is the default when nothing else is established.
1. The ticket Lyra named in the request.
2. The current branch name — `task/WSP-32542/remove-wspgov-terraform` gives `WSP-32542`. Extract it: `git branch --show-current | grep -oE '[A-Z]{2,}-[0-9]+'`.
3. The ticket the branch's existing commits already use.
If none of those yield an ID, ask which ticket to file the work under. Do not guess, do not reuse the ID from an unrelated earlier task in the session, and do not invent a plausible-looking number. Every commit in a branch normally carries the same ID; if the work genuinely spans two tickets, split the commits accordingly rather than picking one at random.
**Exception — repos with no issue tracker.** Personal repos such as `nixfiles` have no Jira project. There the scope is the area of the change, not a ticket: `chore(claude): ...`, `chore(deps): ...`, `feat(hosts): ...`. Conventional form is still required; only the ticket scope is dropped. Never invent a WSP number to satisfy the rule in a repo that has no tickets. The ticket requirement applies to the work repos under `~/code` that are backed by the WSP Jira project and gated by CI.
**This format is mandatory and overrides the repo's existing log style.** Many repos (`multicluster`, `core-services-cloud`) have histories full of bare `<TICKET-ID>: summary` subjects written by other people. Do not copy that. Match repo style for _branch names_ only; commit subjects are always full Conventional Commits with the ticket scope. CI enforces this, and a failure means Lyra rebases the history by hand.
**Known failure mode — scope decay across a session.** The first commit gets `fix(<TICKET-ID>): ...` correctly, then follow-up commits in the same sitting degrade to bare `test: add tests for class`, `refactor: hoist middleware`, `chore: tidy`. This has caused real rebase work in `core-services-cloud`. The second, third and fifth commits need the ticket scope exactly as much as the first. Re-read the subject against the format before every single `git commit`.
**Merge commits count too.** Prefer `git rebase origin/<base>` over `git merge` so none is created. If unavoidable, set the message explicitly: `git merge --no-ff -m "<TICKET-ID>: merge master into <branch>"`. Keep the ID uppercase; the check is case-sensitive.
**Before pushing, verify — do not skip this:**
```
git log --format=%s origin/<base>..HEAD | grep -vE '^[a-z]+(\([A-Z]{2,}-[0-9]+\))!?: '
```
Must print nothing. Writing each subject carefully is not a substitute for running it.
**Auditing past behaviour is unreliable.** If Lyra has already rebased to fix a bad subject, the log shows her corrected version, not what was originally written. A clean `git log` is not evidence that nothing was wrong. Check author date vs committer date (`--format="%ad %cd"`) — a mismatch means history was rewritten. Never argue from a clean log that the fault did not occur.
**Why:** Lyra's standard workflow for traceability, and a hard CI gate. A malformed subject is manual rebase work for her, not just a red build.
**How to apply:** Conventional form on every commit in every repo; the ticket scope additionally on every commit in a Jira-backed work repo. Format first, repo style second. Run the verification grep before every push. Relates to [[git_check_state]].
@@ -0,0 +1,76 @@
---
name: wsp-local-build-and-test
description: "How to compile and test core-services-cloud locally on Lyra's NixOS/WSL box: dotnet via nix, artifactory creds from ~/.artifactoryenv, sourced per command"
metadata:
node_type: memory
type: reference
---
Canonical build/test commands for `core-services-cloud` live in the repo at
`.ai/agents.md` and `.ai/component-tests.md` — read those rather than guessing.
The repo docs assume Windows/PowerShell paths; this box is NixOS under WSL, so
the environment deltas below are what actually make them run.
**dotnet is not on PATH.** Get it from nixpkgs — see [[nix-shell-tooling]]:
```sh
nix shell nixpkgs#dotnet-sdk_8 --command dotnet build
```
`global.json` pins SDK 8 with `rollForward: minor`, so `dotnet-sdk_8` is the
right attribute.
**Every restore needs artifactory credentials.** They live in
`~/.artifactoryenv` (mode 0600) as `ARTIFACTORY_READ_ACCESS_USER` and
`ARTIFACTORY_READ_ACCESS_TOKEN`, consumed by `nuget.config`. Shell state does
not persist between tool calls, so source them inside each command:
```sh
set -a; . ~/.artifactoryenv; set +a
```
**Check the credentials before blaming the code.** A failed restore reports
`NU1301: Unable to load the service index`, which looks like a network fault but
is usually auth. Confirm which it is:
```sh
curl -s -o /dev/null -w '%{http_code}\n' \
-u "$ARTIFACTORY_READ_ACCESS_USER:$ARTIFACTORY_READ_ACCESS_TOKEN" \
https://repo.citrite.net/api/nuget/v3/stf-virtual-nuget/index.json
```
200 means the credentials are good. 401 means the token is the problem, not the
change under test. `https://repo.citrite.net/api/system/ping` returning `OK`
proves reachability independently of auth.
**Component tests** need Docker plus the same credentials, and are driven by
`./service.ps1` — PowerShell, so `nix shell nixpkgs#powershell` if `pwsh` is
missing. Log in to the image registry first:
```sh
echo "$ARTIFACTORY_READ_ACCESS_TOKEN" | docker login stf-virtual-docker.repo.citrite.net \
--username "$ARTIFACTORY_READ_ACCESS_USER" --password-stdin
```
Two Docker Desktop leftovers break this box, both fatal and both easy to miss:
1. `/usr/bin/docker` is a dangling symlink into an absent Docker Desktop WSL
mount, and it shadows the working NixOS docker inside `pwsh`. The script dies
with `Program 'docker' failed to run ... No such file`.
2. `~/.docker/config.json` sets `"credsStore": "desktop.exe"`, a helper that does
not exist. `docker login` reports success while storing nothing, then pulls
fail with `error getting credentials - err: exit status 1`. Remove the
`credsStore` key and log in again; docker then writes the auth into
`config.json` itself.
Put the real docker first when invoking anything that shells out to it, and note
`$PATH` must expand _inside_ the nix shell or dotnet drops off the path:
```sh
nix shell nixpkgs#dotnet-sdk_8 --command sh -c \
'export PATH="/run/current-system/sw/bin:$PATH"; dotnet test ...'
```
A feature canary used by a component test must also be registered in
`Automation/Component/ComponentTests/src/Citrix.Wsp.Test.Mocks/WspComprehensive/__files/unleash/unleash-test-environment.json`,
or `SetFeatureFlag` fails the test as inconclusive rather than failing loudly.
@@ -20,6 +20,25 @@ report? If the latter, rewrite. Retain all software-engineering capability and t
- Refer to the user as "comrade Lyra" when it reads naturally; do not force it into every line.
- No emojis.
## Length and form (the voice fails here first)
Terseness is structural, not just tonal. A dry register wrapped in report furniture —
headers, tables, a full status recap every turn — is the failure mode, and it passes a
tone-only self-check. Enforce:
- Default ceiling around 150 words. Longer only when the content genuinely needs it:
a real analysis, a comparison of options, a requested writeup.
- Headers and tables only for four or more distinct items. Two facts are two sentences.
- Report the delta since the last message, never the accumulated state. Assume Lyra
remembers what she was told.
- State each caveat once per session. Repeating a settled limitation is filler.
- Do the obvious next action and report it. Do not present a menu of options for a
decision that has an obvious answer.
- Do not restate the request, or narrate what is about to be done.
Self-check before sending: is this the delta, at the shortest length that stays accurate?
If it reads like a status report, cut it to the three facts that changed.
## Scope
The persona lives in PROSE ONLY — explanations, summaries, status, discussion. It must NEVER
+3
View File
@@ -8,6 +8,9 @@
./git.nix
./editor.nix
./claude.nix
# Declares services.headlessSecretService; opt-in, off by default. Graphical
# hosts should prefer home-manager's own services.gnome-keyring.
./secret-service.nix
];
# Manage the XDG base-directory layout and ~/.config files. Tools above
+1
View File
@@ -19,6 +19,7 @@
pkgs.element-desktop
pkgs.legcord
pkgs.nemo # file manager (launched via Mod+e, see ./sway.nix)
pkgs.darktable
#pkgs.plex-desktop
#pkgs.plexamp
];
+13
View File
@@ -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;
+142
View File
@@ -0,0 +1,142 @@
# Headless Secret Service (org.freedesktop.secrets) on the user session bus,
# for CLI tools that keep credentials in the system keychain rather than in a
# config file of their own.
#
# Current consumer: gcx, the Grafana Cloud CLI (users/emmathorpe/work.nix). gcx
# stores its OAuth access and refresh tokens in the keychain unconditionally --
# its config file holds only opaque `keychain:gcx:v2:...` handles -- and offers
# no plaintext fallback (there is no environment variable or config key to
# select a file-backed store). With nothing owning org.freedesktop.secrets,
# `gcx login` authenticates against Grafana successfully and then dies writing
# its config: "The name is not activatable".
#
# home-manager already ships services.gnome-keyring, but it does not fit a
# headless host on two counts:
#
# * it is WantedBy graphical-session-pre.target, which never activates
# without a desktop session, so the service would simply never start; and
# * it cannot unlock the login keyring (it passes no --unlock). An unlocked
# collection is mandatory: writing to a locked one blocks on a GUI prompter
# (gcr) that does not exist here, so the caller hangs rather than fails.
#
# Security posture, stated plainly: the login keyring is encrypted at rest, but
# the password unlocking it is readable by the same user on the same machine.
# That protects the tokens from something reading the keyring file directly; it
# protects them from nothing already running as this user. It is the same
# posture as the existing ~/.jenkinsenv and ~/.splunkenv token files, and it is
# the price of unattended operation -- systemd --user timers start with no
# human present to type a passphrase.
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.headlessSecretService;
# Where the generated unlock password lives when no external passwordFile is
# supplied. Under $XDG_DATA_HOME rather than the nix store, which is
# world-readable.
defaultPasswordFile = "${config.xdg.dataHome}/gnome-keyring/login-password";
passwordFile = if cfg.passwordFile != null then cfg.passwordFile else defaultPasswordFile;
keyringDaemon = pkgs.writeShellApplication {
name = "headless-secret-service";
runtimeInputs = [
pkgs.gnome-keyring
pkgs.coreutils
];
text = ''
pwfile=${lib.escapeShellArg passwordFile}
if [ ! -s "$pwfile" ]; then
echo "headless-secret-service: no keyring password at $pwfile" >&2
exit 1
fi
# The daemon takes the whole of stdin as the password, so a trailing
# newline would silently become part of it. Strip it, so a hand-written or
# agenix-managed file unlocks the same keyring the generated one created.
#
# --components=secrets ONLY. The ssh component must stay off: it would
# claim SSH_AUTH_SOCK and displace services.ssh-agent, breaking SSH auth
# and signed commits. pkcs11 is not needed by anything here.
tr -d '\n' <"$pwfile" |
exec gnome-keyring-daemon --foreground --components=secrets --unlock
'';
};
in
{
options.services.headlessSecretService = {
enable = lib.mkEnableOption ''
a headless gnome-keyring serving org.freedesktop.secrets on the user
session bus, with the login keyring unlocked at service start'';
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/run/agenix/gnome-keyring-login";
description = ''
Path to a file holding the login keyring password. It is read at service
start, not at build time, so it need not exist when the system is built
-- this is the seam for an agenix-managed secret.
When null, a random 32-byte password is generated on first activation at
${defaultPasswordFile} (mode 0600) and reused from then on.
Pointing this at a different file after the login keyring already exists
does NOT re-key the keyring: the daemon will fail to unlock it. To
change the password, delete ~/.local/share/keyrings and re-authenticate
every tool that stored a secret there.
'';
};
};
config = lib.mkIf cfg.enable {
# secret-tool, for inspecting or repairing the keyring by hand when a stored
# credential misbehaves (`secret-tool search --all service gcx`).
home.packages = [ pkgs.libsecret ];
# Generate the unlock password on first activation. Guarded on us owning it:
# an externally supplied passwordFile is never created or written here.
home.activation = lib.mkIf (cfg.passwordFile == null) {
headlessSecretServicePassword = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
pwfile=${lib.escapeShellArg defaultPasswordFile}
if [ ! -s "$pwfile" ]; then
run mkdir -p "$(dirname "$pwfile")"
# Create the file empty at 0600 first, then fill it: the redirect
# keeps the existing mode, so the password is never briefly readable.
run install -m 600 /dev/null "$pwfile"
run ${pkgs.bash}/bin/sh -c \
'head -c 32 /dev/urandom | base64 -w0 > "$1"' sh "$pwfile"
fi
'';
};
systemd.user.services.headless-secret-service = {
Unit = {
Description = "GNOME Keyring (Secret Service, headless)";
Documentation = "man:gnome-keyring-daemon(1)";
# The daemon claims its name on the user session bus.
Requires = [ "dbus.socket" ];
After = [ "dbus.socket" ];
};
Service = {
Type = "simple";
ExecStart = lib.getExe keyringDaemon;
Restart = "on-failure";
RestartSec = 2;
};
# default.target, not graphical-session-pre.target: there is no graphical
# session on this host. With `linger` enabled (see the host table in
# flake.nix) default.target is reached at boot, so the keyring is also up
# for unattended systemd --user timers, not just interactive logins.
Install.WantedBy = [ "default.target" ];
};
};
}
+44
View File
@@ -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
# ../docs/shell.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 ../docs/shell.md, "Replacing the classics".
#
# Blast radius is bounded by where these live: shellAliases lands in
# .zshrc, so only interactive zsh sees them. Scripts, `sudo <cmd>` 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.
};
};
+2
View File
@@ -98,6 +98,7 @@
"lld@21"
"python@3.14"
"dosbox-staging"
"mole"
];
# GUI applications. macOS app bundles are managed as casks; nixpkgs darwin
# GUI support is unreliable, so these stay on brew for continuity.
@@ -111,6 +112,7 @@
"bitwarden"
"citrix-workspace"
"curseforge"
"darktable"
"discord"
"firefox"
"freecad"
-61
View File
@@ -1,61 +0,0 @@
# Mac Pro 3,1 (Early 2008) — install notes
Flake host: `lyrathorpe-macpro31`. Desktop (`portable = false`, imports
`../../modules/desktop.nix`). Files: `configuration.nix`,
`hardware-configuration.nix`.
## Hardware configuration
`hardware-configuration.nix` here is the real config generated by
`nixos-generate-config` on the machine. Root is an **LVM** logical volume
(`/dev/mapper/MacPro-Root`, ext4); the ESP (vfat) and swap are referenced by
UUID. The initrd carries `dm-snapshot` for the LVM root. Regenerate and commit
if the disk layout changes.
## Bootloader
The Mac Pro 3,1 has **64-bit EFI**, so it uses **systemd-boot** (no GRUB/CSM
shim). `canTouchEfiVariables = false` because Apple's firmware does not reliably
accept `efibootmgr` NVRAM writes.
Apple-EFI quirk: if the firmware boot picker does not show NixOS after install,
either
- uncomment `boot.loader.efi.efiInstallAsRemovable = true;` in
`configuration.nix` (installs the fallback `\EFI\BOOT\BOOTX64.EFI`), and/or
- "bless" the ESP from macOS.
Partition the disk GPT with an ESP (vfat).
## Graphics
The stock card varies between units — **ATI Radeon HD 2600 XT** or **NVIDIA
GeForce 8800 GT**. No proprietary driver is hardcoded; Sway relies on in-tree KMS:
- ATI Radeon HD 2600 XT → `radeon` (or `amdgpu`) KMS
- NVIDIA GeForce 8800 GT → `nouveau` KMS
These come up automatically. If a card needs forcing, set
`services.xserver.videoDrivers` and/or add the module to
`boot.initrd.kernelModules` for early KMS (see the comment in
`configuration.nix`).
## Networking
Wired Ethernet via NetworkManager (from `desktop.nix`) — the Mac Pro has two
gigabit ports.
## Login
Graphical login via a Wayland greeter — `greetd` running ReGreet inside the
`cage` kiosk compositor — configured centrally in `../../modules/sway.nix` for
every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced
to the Dvorak layout to match the console and Sway session. Set the user
password (`passwd lyrathorpe`) after install, or the greeter cannot
authenticate. Requires working KMS (radeon/nouveau — see Graphics).
## Apply
```sh
sudo nixos-rebuild switch --flake .#lyrathorpe-macpro31
```
+10 -12
View File
@@ -1,14 +1,20 @@
# Apple Mac Pro 3,1 (Early 2008, dual Xeon Harpertown, x86_64). Desktop host:
# shared graphical/wired options live in ../../modules/desktop.nix; only
# host-specific settings are here. Install notes (EFI booting, GPU, partitions):
# see ./README.md.
# see ../../docs/hosts/macpro31.md.
{ ... }:
{
imports = [
./hardware-configuration.nix
./nvidia.nix
];
# Dual quad-core Xeon (Harpertown/Penryn): SSE4.1 but no SSE4.2 or POPCNT,
# i.e. x86-64-v1. Declaring it here switches off the fleet flags that need a
# newer CPU -- currently features.claudeCode (see ../../modules/features.nix).
features.cpu.microarchLevel = 1;
# The Mac Pro 3,1 has 64-bit EFI (confirmed by the owner), so boot via
# systemd-boot like the MBP -- no GRUB/BIOS shim needed.
boot.loader.systemd-boot.enable = true;
@@ -33,17 +39,9 @@
# enabled in workstation.nix.
hardware.cpu.intel.updateMicrocode = true;
# GPU note: the stock card varies between units -- ATI Radeon HD 2600 XT or
# NVIDIA GeForce 8800 GT. Sway needs a working KMS/modesetting driver; do NOT
# install a proprietary blob here. Depending on the installed card, rely on
# the open kernel driver:
# - ATI Radeon HD 2600 XT -> "radeon" (older) or "amdgpu" KMS
# - NVIDIA GeForce 8800 GT -> "nouveau" KMS
# These come up automatically via the in-tree drivers + KMS, and the graphics
# stack itself is enabled by modules/sway.nix. If a card needs to be forced, add it
# here, e.g. `services.xserver.videoDrivers = [ "radeon" ];` (or "nouveau"),
# and/or `boot.initrd.kernelModules = [ "radeon" ];` in
# hardware-configuration.nix for early KMS.
# GPU: the stock card (ATI Radeon HD 2600 XT / NVIDIA GeForce 8800 GT) has
# been replaced with an NVIDIA Quadro P400. Driver, Wayland quirks and
# GPU-enabled Docker live in ./nvidia.nix.
# See `man configuration.nix` / the stateVersion docs before changing.
system.stateVersion = "26.05";
+66
View File
@@ -0,0 +1,66 @@
# NVIDIA Quadro P400 (Pascal, GP108) on the Mac Pro 3,1: proprietary driver for
# the Sway desktop, plus Docker with GPU/CUDA access for containers.
#
# Driver branch: 580 (nvidiaPackages.legacy_580), NOT the nixpkgs default
# (`production`, currently 595.x). 580 is the last branch that supports
# Maxwell/Pascal/Volta -- NVIDIA keeps it as an LTS branch to Aug 2028 -- and a
# newer branch simply will not drive this card.
#
# The driver is unfree, so it is not in the binary cache: the kernel module is
# compiled locally. On this machine's 2008 Xeons expect the first rebuild after
# a kernel bump to take a long while.
{ config, ... }:
{
# Selects the proprietary driver; the module blacklists nouveau/nvidiafb and
# loads nvidia-uvm (needed by CUDA) via modprobe softdep. Naming is historical
# -- this option drives the kernel/driver choice on Wayland hosts too, which
# is why it is set on a machine that runs no X server.
services.xserver.videoDrivers = [ "nvidia" ];
hardware.nvidia = {
package = config.boot.kernelPackages.nvidiaPackages.legacy_580;
# Required for Wayland: sets nvidia-drm.modeset=1 (and fbdev=1), without
# which wlroots gets no GBM device and Sway/cage fail to start.
modesetting.enable = true;
# The open kernel modules need Turing or later; Pascal must use the closed
# ones. Explicit because the option has no default on driver >= 560.
open = false;
};
# The NVIDIA module only puts these in boot.kernelModules when
# services.xserver.enable is true, which is false on this Wayland-only host --
# so load them explicitly rather than relying on udev modalias autoloading.
# nvidia_uvm (needed by CUDA) is deliberately absent: the module's modprobe
# softdep pulls it in after the GPU device exists, which is the supported
# ordering.
boot.kernelModules = [
"nvidia"
"nvidia_modeset"
"nvidia_drm"
];
# wlroots refuses the proprietary NVIDIA driver unless told to proceed. The
# greeter's compositor (cage) has no such check; only Sway needs the flag,
# which the module bakes into the wrapper the session's .desktop file runs.
programs.sway.extraOptions = [ "--unsupported-gpu" ];
virtualisation.docker.enable = true;
# CDI-based GPU access for containers: generates /var/run/cdi specs from the
# host driver at boot and turns on Docker's CDI feature. Run GPU workloads
# with `docker run --device=nvidia.com/gpu=all ...`. The deprecated
# virtualisation.docker.enableNvidia runtime wrapper is deliberately not used.
hardware.nvidia-container-toolkit.enable = true;
# The generator needs a loaded kernel module: without one it aborts with
# "failed to initialize NVML: Driver Not Loaded". That is guaranteed after a
# kernel bump, where the rebuilt module cannot load until reboot -- and since
# the unit is requiredBy docker.service and wantedBy multi-user.target, the
# failure takes Docker down and makes `nixos-rebuild switch` exit non-zero.
# Skip the run instead when no driver is loaded; the toolkit's udev rule
# restarts the unit as soon as the nvidia device appears, so the CDI specs are
# still generated on the next boot.
systemd.services.nvidia-container-toolkit-cdi-generator.unitConfig.ConditionPathExists =
"/proc/driver/nvidia/version";
}
+111
View File
@@ -0,0 +1,111 @@
# Raspberry Pi Zero 2 W (aarch64) "Psion sidecar": an RS232 companion for a
# Psion 5MX. Two roles, split into submodules: ./serial-ppp.nix (PPP over the
# serial line, NAT out to wifi, telnet login) and ./email-proxy.nix (cleartext
# POP3/SMTP front end for the Psion's mail client). The raspberry-pi-3
# nixos-hardware profile (the Zero 2 W is the same BCM2837 SoC as the Pi 3) and
# key-only sshd (../../modules/ssh.nix) are layered on in the flake host table.
# Install notes: see ../../docs/hosts/pizero2w.md.
{ lib, ... }:
{
imports = [
./hardware-configuration.nix
./serial-ppp.nix
./email-proxy.nix
];
# Match the flake's nixosConfigurations attribute name so `nh os switch`
# (which selects by the local hostname) resolves without an explicit -H flag.
networking.hostName = "lyrathorpe-zero2w";
# Headless server: modules/sway.nix is not imported and
# features.swayDesktop.enable defaults to false, so this host keeps plain
# TTY/SSH login.
# Claude Code is a Node application. It runs on aarch64, but not usefully in
# 512 MB of RAM, and its closure is unwelcome on an SD card.
features.claudeCode.enable = false;
# 512 MB total and no swap partition -- SD cards wear out under swap writes.
# Compressed RAM swap instead; zstd is the best ratio-per-cycle the SoC can
# sustain.
zramSwap = {
enable = true;
algorithm = "zstd";
};
# The NixOS manual and man page index cost build time and a chunk of the card
# for a box that is administered over SSH from elsewhere.
documentation.nixos.enable = false;
# Own the firmware partition declaratively: every switch rewrites config.txt,
# the vendor device trees and the overlays below. Without this the card keeps
# whatever config.txt the flashed image wrote and the UART overlays never
# load. uboot.enable keeps the GPU firmware chainloading U-Boot -> extlinux,
# which is how the NixOS aarch64 SD image boots; leaving it off would rewrite
# config.txt without a `kernel=` line and the board would stop booting.
hardware.raspberry-pi.firmware = {
enable = true;
uboot.enable = true;
};
hardware.raspberry-pi.configtxt = {
settings.all = {
# Headless: hand the VideoCore the minimum and leave the rest to Linux.
# start_x/camera_auto_detect otherwise reserve VRAM for a camera stack
# this board does not have.
gpu_mem = 16;
start_x = 0;
camera_auto_detect = false;
# Left on, the firmware auto-loads the KMS display overlay, which wants
# more VRAM than this board can spare for a monitor it will never have.
display_auto_detect = false;
};
# Replaces the profile's default (vc4-kms-v3d), which is display hardware
# this host never uses.
deviceTreeOverlays.all = [
# Move the PL011 UART off Bluetooth and onto GPIO 14/15, so /dev/ttyAMA0
# is the RS232 header. The mini UART (ttyS0) derives its baud rate from
# the core clock and drifts at 115200.
{ disable-bt = { }; }
# RTS/CTS on GPIO 16/17: the Psion's modem profile uses hardware flow
# control, and so does pppd in ./serial-ppp.nix.
{ uart0.ctsrts = true; }
];
};
# Wifi is the Pi's uplink and the route the Psion reaches the internet over
# (./serial-ppp.nix masquerades onto it).
networking.interfaces.wlan0.useDHCP = true;
networking.wireless = {
enable = true;
interfaces = [ "wlan0" ];
# PSKs stay out of the Nix store: wpa_supplicant reads them at runtime from
# this file, which is created on the device (root-owned, 0600) and contains
# psk_home=<the pre-shared key>
# See ../../docs/hosts/pizero2w.md.
secretsFile = "/var/lib/wpa_supplicant/secrets.conf";
networks."CHANGE-ME-SSID".pskRaw = "ext:psk_home";
};
# The board takes a DHCP lease over wifi, so its address moves. mDNS makes it
# findable as lyrathorpe-zero2w.local instead of hunting through the router's
# lease table -- which matters most on first boot, when it is the only way in.
services.avahi = {
enable = true;
openFirewall = true;
publish = {
enable = true;
addresses = true;
workstation = true;
};
};
# Default-deny inbound. sshd opens 22 (../../modules/ssh.nix); everything the
# Psion talks to is reached over the PPP link, which ./serial-ppp.nix marks
# trusted.
networking.firewall.enable = true;
# See `man configuration.nix` / the stateVersion docs before changing.
system.stateVersion = "26.05";
}
+25
View File
@@ -0,0 +1,25 @@
# legacy-email-proxy: a cleartext POP3 (110) and SMTP (25) front end for the
# Psion's built-in mail client, forwarded to authenticated IMAPS/SMTPS.
#
# The package, the systemd unit and its hardening all live upstream
# (https://code.emmathe.dev/lyrathorpe/legacy-email-proxy); this host only
# enables the service and points it at the credentials.
{ inputs, ... }:
{
imports = [ inputs.legacy-email-proxy.nixosModules.default ];
services.legacy-email-proxy = {
enable = true;
# The listeners are unauthenticated and unencrypted by design, so the
# firewall is what confines them: ppp0 is trusted, wlan0 is not, and 110/25
# are never opened there (./serial-ppp.nix). They stay on the default
# 0.0.0.0 rather than the PPP address because 10.0.0.1 exists only while
# the Psion is plugged in, and a bind-time dependency on a serial cable is
# a restart loop waiting to happen.
# Backend hostnames and credentials. Kept out of the Nix store: created on
# the device, root-owned 0600. See ../../docs/hosts/pizero2w.md.
environmentFile = "/var/lib/legacy-email-proxy/backend.env";
};
}
+33
View File
@@ -0,0 +1,33 @@
# PLACEHOLDER hardware configuration for the Raspberry Pi Zero 2 W.
#
# This file is NOT the real generated config -- it exists only so the host
# evaluates in CI before the Pi is provisioned. The machine will not boot from
# it as-is. On first install, regenerate this file on the device with
# nixos-generate-config --root /mnt
# and replace this placeholder with the output (commit it). See ../../docs/hosts/pizero2w.md.
#
# Like every hardware-configuration.nix in this repo, this file is excluded from
# the formatter and linters (see the pre-commit/treefmt excludes in flake.nix).
{ modulesPath, ... }:
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
nixpkgs.hostPlatform = "aarch64-linux";
# The Zero 2 W boots from an SD card with a FAT firmware partition and an ext4
# root. Labels match the conventional sd-image layout; the real generated
# config will use by-uuid device paths instead.
fileSystems."/" = {
device = "/dev/disk/by-label/NIXOS_SD";
fsType = "ext4";
};
fileSystems."/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
};
# 512 MB of RAM and an SD card: no swap partition (SD cards wear out under
# swap writes). zram takes its place; see ../../hosts/PiZero2W/configuration.nix.
swapDevices = [ ];
}
+44
View File
@@ -0,0 +1,44 @@
# SD-card image of this host, used exactly once: to bring the board up.
#
# Deliberately NOT imported by ./configuration.nix. The flake extends the host
# with it (see packages.aarch64-linux.zero2w-sd-image in ../../flake.nix), so
# the card carries the host's own kernel, config.txt and SSH keys rather than a
# generic installer that then has to be reconfigured over a console this host
# does not have -- pppd owns the serial port (./serial-ppp.nix).
#
# It does not carry the runtime secrets. Seed those into the card's root
# partition before first boot; see ../../docs/hosts/pizero2w.md.
{
config,
lib,
modulesPath,
...
}:
{
imports = [ "${modulesPath}/installer/sd-card/sd-image.nix" ];
# sd-image.nix pulls in profiles/all-hardware.nix, which is every driver and
# firmware blob NixOS knows about. The raspberry-pi-3 profile already carries
# what this board has, and the card is small.
hardware.enableAllHardware = lib.mkForce false;
image.baseName = "nixos-zero2w";
sdImage = {
# Compressing costs a long single-threaded pass and buys nothing: the image
# is written straight to a card with dd.
compressImage = false;
# The default 30 MiB does not hold the vendor GPU firmware, U-Boot and the
# BCM2837 device trees and overlays that nixos-hardware installs here.
firmwareSize = 128;
# The firmware partition is populated by nixos-hardware's firmware module
# (it takes over sdImage.populateFirmwareCommands); the root side is the
# stock extlinux install, which no longer arrives with it.
populateRootCommands = ''
mkdir -p ./files/boot
${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
'';
};
}
+89
View File
@@ -0,0 +1,89 @@
# The serial half of the Psion sidecar: a PPP link to a Psion 5MX over
# /dev/ttyAMA0 (RS232 level shifter on the GPIO header, 115200 8N1 with
# RTS/CTS), masqueraded out of wifi, plus a telnet login for the Psion's
# terminal client.
#
# Cleartext telnet and unauthenticated PPP are safe *only* because the link is
# a two-node cable: the peer is a machine from 1999 that speaks no TLS. Nothing
# here is exposed to wlan0.
{ pkgs, ... }:
let
# Point-to-point addresses for the serial link; nothing else routes here.
piAddress = "10.0.0.1";
psionAddress = "10.0.0.2";
in
{
# pppd needs exclusive use of the port. NixOS starts a getty on any serial
# console named in boot.kernelParams; ttyAMA0 is not one today, but disable it
# explicitly so a later kernel-param change cannot silently steal the line.
systemd.services."serial-getty@ttyAMA0".enable = false;
services.pppd = {
enable = true;
peers.psion.config = ''
/dev/ttyAMA0
115200
${piAddress}:${psionAddress}
# Hardware flow control, matching the Psion's modem profile.
crtscts
# A null-modem cable has no carrier detect and no peer to authenticate.
local
noauth
# The systemd unit is Type=notify, so pppd must stay in the foreground.
nodetach
lock
# Wait for the Psion rather than failing when it is unplugged, and keep
# waiting for the next time it is plugged back in.
passive
persist
maxfail 0
holdoff 1
# Hand the Psion resolvers over the link, so its Internet profile can set
# "get DNS from server = True" instead of hard-coding them.
ms-dns 1.1.1.1
ms-dns 8.8.8.8
'';
};
# The Psion's route to the internet. The original write-up used pppd's
# proxyarp instead; NAT keeps the Psion out of the LAN broadcast domain and
# does not depend on what the wifi router tolerates.
networking.nat = {
enable = true;
externalInterface = "wlan0";
internalIPs = [ "${psionAddress}/32" ];
};
# Everything the Psion connects to (telnet here, POP3/SMTP in
# ./email-proxy.nix) is reachable over the PPP link and nowhere else.
networking.firewall.trustedInterfaces = [ "ppp0" ];
# The Psion's terminal client speaks telnet over TCP, which it renders far
# better than the raw serial console. Socket-activated, one process per
# connection; busybox's telnetd in inetd mode hands straight over to login.
systemd.sockets.telnetd = {
description = "Telnet login socket for the Psion";
wantedBy = [ "sockets.target" ];
listenStreams = [ "${piAddress}:23" ];
socketConfig = {
Accept = true;
# ppp0 (and with it 10.0.0.1) only exists while the Psion is connected;
# FreeBind lets the socket be listening before that.
FreeBind = true;
};
};
systemd.services."telnetd@" = {
description = "Telnet login for the Psion";
serviceConfig = {
ExecStart = "-${pkgs.busybox}/bin/busybox telnetd -i -l ${pkgs.shadow}/bin/login";
StandardInput = "socket";
StandardError = "journal";
};
};
}
+1 -1
View File
@@ -2,7 +2,7 @@
# ./docker.nix (Docker host with a network socket) and ./reverse-proxy.nix
# (native nginx). The raspberry-pi-5 nixos-hardware profile (kernel, firmware,
# device tree) and key-only sshd (../../modules/ssh.nix) are layered on in the
# flake host table. Install notes: see ./README.md.
# flake host table. Install notes: see ../../docs/hosts/rpi5.md.
{ ... }:
{
imports = [
+1 -1
View File
@@ -4,7 +4,7 @@
# evaluates in CI before the Pi is provisioned. The machine will not boot from
# it as-is. On first install, regenerate this file on the device with
# nixos-generate-config --root /mnt
# and replace this placeholder with the output (commit it). See ./README.md.
# and replace this placeholder with the output (commit it). See ../../docs/hosts/rpi5.md.
#
# Like every hardware-configuration.nix in this repo, this file is excluded from
# the formatter and linters (see the pre-commit/treefmt excludes in flake.nix).
+1 -1
View File
@@ -1,6 +1,6 @@
# ThinkPad T400 (NixOS). Shared laptop options live in ../../modules/laptop.nix;
# only host-specific settings are here. Install notes (boot variants, GPU,
# partitions): see ./README.md.
# partitions): see ../../docs/hosts/t400.md.
{ config, ... }:
{
+16
View File
@@ -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; [
+67 -2
View File
@@ -6,7 +6,72 @@
# headless host (e.g. the Pi) must be able to leave it at its default without
# pulling in modules/sway.nix. The implementation lives in modules/sway.nix,
# gated on this flag.
{ lib, ... }:
#
# The file also carries the host capability facts those flags derive from
# (features.cpu.*). features.claudeCode.enable is such a derived flag: it is
# computed from the declared CPU level here and read by home/claude.nix through
# home-manager's osConfig, so a machine that cannot run the tool never installs
# it, on any host, without per-host opt-outs.
{
options.features.swayDesktop.enable = lib.mkEnableOption "the Sway desktop";
config,
lib,
pkgs,
...
}:
let
cfg = config.features;
# Claude Code runs on Node, whose V8 build requires SSE4.2 and POPCNT -- the
# x86-64-v2 feature set. On an older x86_64 CPU it does not run (illegal
# instruction), so it must not be installed there.
claudeCodeMinLevel = 2;
claudeCodeSupported =
!pkgs.stdenv.hostPlatform.isx86_64 || cfg.cpu.microarchLevel >= claudeCodeMinLevel;
in
{
options.features = {
swayDesktop.enable = lib.mkEnableOption "the Sway desktop";
cpu.microarchLevel = lib.mkOption {
type = lib.types.ints.between 1 4;
default = 2;
example = 1;
description = ''
The x86-64 psABI microarchitecture level the host CPU implements:
1 = the original baseline, 2 = SSE4.2/POPCNT (Nehalem, 2008+),
3 = AVX2, 4 = AVX-512.
Nix cannot detect this (evaluation is pure and hosts are often built
elsewhere), so a machine older than the default declares its own level
and the flags below derive from it. Ignored on non-x86_64 hosts.
'';
};
claudeCode.enable = lib.mkOption {
type = lib.types.bool;
default = claudeCodeSupported;
defaultText = lib.literalMD ''
`true`, unless the host declares an x86-64 microarchitecture level
below ${toString claudeCodeMinLevel}
'';
description = ''
Whether to install Claude Code in this host's home-manager profiles
(implemented in home/claude.nix). Defaults off on CPUs below
x86-64-v${toString claudeCodeMinLevel}, which cannot run it; forcing it
on such a host is an evaluation error.
'';
};
};
config.assertions = [
{
assertion = cfg.claudeCode.enable -> claudeCodeSupported;
message = ''
features.claudeCode.enable is on, but this host declares
features.cpu.microarchLevel = ${toString cfg.cpu.microarchLevel}.
Claude Code needs x86-64-v${toString claudeCodeMinLevel}
(SSE4.2/POPCNT) and will not run on an older CPU.
'';
}
];
}
+21
View File
@@ -46,8 +46,29 @@
pkgs.tflint # Terraform linter (catches what terraformls won't)
pkgs.terraform-docs # generate Terraform module docs
pkgs.yq-go # jq for YAML
pkgs.gcx # Grafana Cloud CLI (dashboards, SLOs, synthetics, alerts)
];
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
# home-manager's services.gnome-keyring cannot be used on a headless host,
# and for the security trade-off of an auto-unlocked keyring.
services.headlessSecretService.enable = true;
home.shellAliases = {
docker = "/run/current-system/sw/bin/docker";
};