NixOS flake for all my machines: Hyprland (two shell variants — waybar and Caelestia/Quickshell) plus Niri + Noctalia as a fallback, Ghostty, Neovim, tmux, and a dev-first setup with local LLMs. Built on flake-parts; home-manager runs as a NixOS module.
| Host | Hardware | User | Notes |
|---|---|---|---|
| atlas | Surface Laptop Studio, i7, 32GB RAM | carter | nixos-hardware Surface module, CPU-only Ollama |
| kronos | Ryzen 9 9950X, RTX 5070 12GB, 64GB RAM | cjm | LUKS + btrfs, GPU Ollama + llama.cpp heavy mode |
git clone https://github.com/cartermccann/dotfiles ~/dotfiles
# Validate without sudo
nh os build ~/dotfiles
# Apply (or use the `nrs` alias)
sudo nixos-rebuild switch --flake ~/dotfiles#kronosupdate (alias) rebuilds with updated flake inputs.
flake.nix # inputs; outputs delegated to parts/
parts/ # flake-parts modules: hosts (mkHost), templates
hosts/ # per-machine configuration + hardware config
modules/ # NixOS system modules (desktop, nvidia, audio, ollama, oom-protection, ...)
home/ # home-manager modules (shell, tools, neovim, tmux, niri, hyprland, ...)
lib/ # overlays, the Ouranos palette (night/day), llm-models.nix
pkgs/ # custom package definitions (codex, qmd, playwright-cli, ...)
templates/ # dev-shell templates for `nix flake init -t ~/dotfiles#<lang>`
skills/ # Claude skills owned by this repo; installed by the module that uses them
config/ # generated blobs kept as real files: nvim, hyprland CSS, audio presets, hermes
scripts/ # shell scripts referenced by modules
docs/ # project specifications, architecture, and delivery plans
wallpaper/
config/nvim/ is symlinked out of the store rather than copied into it, so it stays live-editable. The other config/ subdirectories are inputs that are too large to keep inline in Nix strings.
- Create
hosts/<hostname>/with aconfiguration.nix(import the modules you need) and the machine'shardware-configuration.nix. - Register it in
parts/hosts.nix:flake.nixosConfigurations.myhost = mkHost "myhost" { user = "myuser"; };
sudo nixos-rebuild switch --flake ~/dotfiles#myhost
- Hyprland — primary. Hyprland 0.55 Lua config in
home/hyprland/+lib/palette.nix. Two tiles from one compositor config: Hyprland (waybar + swaync + fuzzel + swayosd + hyprlock — the daily driver) and Hyprland (Caelestia) (Caelestia/Quickshell replaces all of those with one process). Both are generated bymkHyprlandLuainhome/hyprland/compositor.nix, which takes ashellKindand diverges at only eight seams — layer rules, launcher/clipboard, notifications, lock/session menu, shell reload, volume/brightness, locked media keys, and autostart. - Niri (Noctalia) — kept as a working fallback; config in
home/niri-noctalia.nix. The standalone plain-Niri session was retired and its login tile is filtered out inmodules/desktop-wayland.nix.
All sessions share ghostty, tmux, the shared Wayland base in modules/desktop-wayland.nix, and the helper scripts in home/niri.nix.
Colours come from lib/palette.nix — Ouranos, cobalt on near-black (night)
or near-white (day), named for the sky Atlas holds up and the father Kronos
was born to. Both variants are always defined; active picks which one the
session components get. Caelestia does its own theming (it derives a Material
scheme) and is deliberately not wired to it.
The EQ is a PipeWire filter-chain sink declared in modules/audio.nix, not EasyEffects — the presets it was ported from are kept in config/audio/ as the record of the curve. Sinks are ranked by priority.session there, but wpctl set-default, pavucontrol, or hypr-audio-sink write a default.configured.audio.sink entry to ~/.local/state/wireplumber/default-nodes that overrides that ranking permanently. If the default sink is wrong, check that file before touching priorities.
Local speech-to-text through sherpa-onnx Parakeet, typed into the focused window with ydotool. Scripts in home/dictation.nix; the ydotoold daemon (root, for /dev/uinput) in modules/dictation.nix.
| Binding | Mode | Model |
|---|---|---|
Super+Alt+L |
live — types words as you speak | Parakeet streaming 1120 ms, int8 |
Super+Alt+Shift+L |
batch — types the whole utterance on stop | Parakeet TDT 0.6B v2 offline, int8 |
Both models (~480 MB each) are fetched on first use into ~/.local/share/parakeet-dictation/, or up front with setup-dictation.sh. The live pipeline logs to ~/.local/state/parakeet-dictation/live.log.
Typing runs at 10 ms/character rather than ydotool's 40 ms default, and on its own thread. Both matter: typing and decoding share one real-time budget, and at stock timing the live pipeline loses that race under load and falls permanently behind rather than catching up.
Ollama runs in a podman container (modules/ollama.nix); models are preloaded per host tier via local.ollamaTier:
| Tier | Hardware | Models |
|---|---|---|
| high | 12GB VRAM | gemma4:12b-it-qat, qwen2.5-coder:3b-base |
| medium | CPU, 32GB RAM | llama3.2:3b |
| low | smaller | llama3.2:3b |
Model tags live in lib/llm-models.nix. gemma4 is the only chat model (the
ai alias); qwen2.5-coder is FIM tab-completion for
minuet in nvim and is never used interactively.
heavy / heavy-stop (fish functions) swap the GPU between Ollama and a llama.cpp server running Qwen3.6-35B MoE with expert offload (modules/llama-heavy.nix).
home/hermes.nix provides the runtime toolchain for the Hermes agent, deliberately not packaged as a derivation: Hermes git-clones into ~/.hermes/, builds its own uv venv, and writes new skills and memory to itself at runtime, so the flake supplies the dependencies and stays out of the way. home/hermes-events.nix installs the event/notification scripts from skills/hermes-ops/.
Maintenance loops run as systemd user timers. Each is a thin Nix wrapper — sandbox, credentials, schedule — around a Claude skill in ~/.claude/skills/ that holds the actual behavior. All of them are proposal-only or draft-PR-only; none merges anything.
| Module | Schedule | What it does |
|---|---|---|
self-improve-loop.nix |
every other day 07:07 | Mines Claude Code transcripts for durable feedback under an anti-slop constitution |
codex-self-improve-loop.nix |
every other day 07:27 | Read-only Codex counterpart; writes inert proposal state only |
ci-triage-loop.nix |
08:00 and 15:00 | Diagnoses failed Actions runs; opens draft ci-fix/* PRs only past a reproduce-before/pass-after oracle |
docs-gardener-loop.nix |
Sundays 09:30 | Audits project CLAUDE.md files against real repo state, files human-gated proposals |
Per-language templates live in templates/ and auto-activate via direnv:
nix flake init -t ~/dotfiles#python # node, python, go, rust, elixir, zig, java, c, ruby, deno
echo "use flake" > .envrc && direnv allowShell aliases and fish functions are defined in home/shell.nix.