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 (anarlog, 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.
The same module defines a speech bus: a virtual capture node carrying only applications whose output is human speech. anarlog records the mic on one channel and system audio on the other, and "system audio" would otherwise mean the default sink's monitor — post-mix, so whatever music is playing lands in every meeting transcript.
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, and anarlog's summaries); 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 |
|---|---|---|
pr-babysit-loop.nix |
weekdays 05:30 | Reviews open PRs across the comcreate-io org, one marker-gated comment per PR, Telegram digest |
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 |
home/whisperlivekit.nix runs a local Deepgram-compatible STT server so anarlog can transcribe meetings without shipping audio to a cloud vendor. It is a uv-tool install, not a flake package — upgrade it with uv tool upgrade whisperlivekit.
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.