Claude

Agent Guide — NixOS Dotfiles Flake

Modular NixOS dotfiles and infrastructure flake maintained by alienzj. Forked from hlissner’s dotfiles, heavily extended. This document is the authoritative orientation for all AI agents (Claude, Gemini, Codex, etc.).


Quick Orientation

  • What this is: A NixOS flake composing 16+ hosts from reusable modules, with a custom CLI toolchain (hey), Janet scripting, and impermanent root.
  • How it builds: hey sync (nixos-rebuild), hey sync build-vm (VM test), nix eval .#nixosConfigurations.<host>.config.system.build.toplevel (dry eval).
  • Key constraint: Fully hermetic — zero impure evaluation. No --impure, no getEnv, no HEYENV. Host identity from flake attr name, user/theme from host config.

Repository Layout

flake.nix            Flake entry + 25+ inputs
default.nix          Root module (auto-imports modules/ recursively)
lib/                 Custom Nix library (mkOpt, mapModules, wrapFakeHome, mkFlake)
modules/             Reusable feature modules (the core of this repo)
hosts/               Per-host composition (each host imports its own modules/)
packages/            Local derivations (hey.packages.*)
overlays/            Nixpkgs overlays
config/              Raw dotfiles linked via home.file/home.configFile
bin/                 hey CLI (Janet) + unified binscripts (.zsh)
lib/hey/             Janet runtime modules (defcmd, sys, vars, etc.)
lib/zsh/             Zsh autoload bridge (hey.wm.name, hey.log, etc.)
docs/                Architecture and subsystem documentation

🧠 Agent Self-Study & Real-Time Search Policy

Because this repository uses niche languages (Janet) and complex declarative paradigms (Nix), you must not guess APIs.

  1. For Janet (*.janet):
    • Before writing complex Janet code, use your web search/browser tool to search site:janet-lang.org/docs <query> or read https://janet-lang.org/api/index.html.
    • Do not assume Clojure or Common Lisp standard libraries apply to Janet.
  2. For Nix (*.nix):
    • If unsure about a Nixpkgs function, search https://noogle.dev or search.nixos.org.
    • If unsure about Nix builtins, search https://nix.dev/manual/nix/latest/language/builtins.
  3. For Zsh (*.zsh):
    • Ensure scripts are POSIX compliant where possible, but leverage native Zsh arrays and parameter expansion heavily.
  4. Mandatory Idiom Review:
    • Always read docs/ai-language-idioms.md before executing large refactors in Nix, Janet, or Zsh to avoid common traps (e.g., infinite recursion in Nix, macro abuse in Janet).

Module System

Option DSL (lib/options.nix)

Always use the shorthand forms:

options.modules.<area>.<name> = {
  enable  = mkBoolOpt false;               # boolean toggle
  setting = mkOpt types.str "default";     # typed option
  desc    = mkOpt' types.str "" "Help\\n";  # with description
};

Never use raw mkOption — use mkOpt/mkBoolOpt/mkOpt' from hey.lib.

Module Template

{ hey, lib, config, pkgs, ... }:
with lib;
with hey.lib; let
  cfg = config.modules.<area>.<name>;
in {
  options.modules.<area>.<name> = { ... };
  config = mkIf cfg.enable (mkMerge [ { ... } { ... } ]);
}
  • Gate all behavior with mkIf cfg.enable.
  • Use mkMerge to separate system-level config from home-level file management.
  • Access hey.packages.* for local packages, hey.lib.* for library helpers.
  • Use assertions for invalid option combinations.

Auto-loading

mapModulesRec' ./modules import in default.nix automatically imports every .nix file under modules/. No manual import lists.

Recursive Loading Rules:

  • Skip Directory: Use a .noload file in a directory to skip auto-loading its contents. This is the preferred method when a directory contains internal module parts (like modules/themes/desktop/) that are manually imported elsewhere.
  • Ignore Prefix: Any directory or file starting with an underscore (e.g., _parts/ or _mixins.nix) is also ignored by the auto-loader.
  • Deduplication: Never manually import a file that is already within the auto-load path unless the directory is skipped via .noload. Manual imports of auto-loaded files cause “double-definition” issues (e.g., duplicated strings in configuration files).

Architecture Rules

Themes Decorate, Not Define (Two-Tiered)

  • Tier 1 — Global Defaults: System provides a production-ready baseline using Catppuccin Mocha when no active theme is set.
  • Tier 2 — Active Decoration: Individual themes (e.g., Alucard) surgically override the baseline via modules.theme options.
  • Requirement: The system must be fully functional and aesthetically coherent if modules.theme.active = null.
  • Centralization: All app-specific color/font mapping lives in modules/themes/apps.nix, not in feature modules.

Home Manager Is a File Bridge

Prefer NixOS system options over Home Manager options. Use home.file, home.configFile, home.dataFile as the bridge for user-space files. Never reach for home-manager.users.<name>.<...> directly — the aliases in modules/home.nix handle that.

Hybrid Fake HOME (Jailing)

Non-XDG apps (Steam, LibreWolf, Thunderbird, LMMS) get jailed to ~/.local/user via wrapFakeHome from lib/pkgs.nix. Use mkFakeHomeEntry for additional desktop entries within the fake home. XDG_* variables are not unset, allowing global font/theme sharing.

Profile System

Hosts compose themselves via profile strings:

  • modules.profiles.role"workstation", "server", "portable", "board"
  • modules.profiles.hardware — list like ["cpu/amd" "gpu/nvidia/pascal" "yubikey"]
  • modules.profiles.networks — list like ["ts0" "sz/homelab"]

Package Hierarchy

Three layers, in order of preference:

  1. pkgs.* — Nixpkgs (unstable channel)
  2. pkgs.pkgs-stable.* — Nixpkgs 25.11 fallback
  3. hey.packages.* — Local packages/ derivations (always use explicit hashes for fetchFromGitHub)

Creating Nix Packages (packages/)

When adding a new derivation to packages/, ensure its default.nix follows this pattern:

{
  self,    # Required: allows local packages to reference each other
  lib,
  stdenvNoCC,
  fetchFromGitHub,
  ...      # Recommended: prevents "unexpected argument" errors
}:
stdenvNoCC.mkDerivation rec {
  pname = "...";
  # ...
}

Crucial Rules:

  1. Argument self: You MUST include self in the function arguments. The mkFlake function in lib/nixos.nix calls pkgs.callPackage with { self = self.packages.${system}; }.
  2. Variadic Arguments: Always include ... in the argument list to prevent “unexpected argument” errors when callPackage passes extra context.
  3. Source Fetching: Use explicit hashes (SRI format) for fetchFromGitHub. Use nix-prefetch-url --unpack <url> to find the hash if needed.

Declarative Provisioning (ensures)

For services like PostgreSQL, avoid imperative scripts. Use the ensures pattern:

modules.services.dev.postgresql.ensures = [
  { username = "user"; database = "db"; passwordFile = config.age.secrets.pass.path; }
];

Host Composition

Each host in hosts/ has:

hosts/<name>/
  default.nix              imports + system arch
  modules/
    configuration.nix      the modules = { ... } attribute set  ← main toggle point
    hardware.nix           hardware/disks
    storage.nix            disko schema
    virt.nix               virtualization
    secrets.nix            agenix references

Keep host files thin. If logic may be shared, extract it into modules/.


Hey Toolchain

hey is the central orchestrator (Janet CLI at bin/hey):

CommandPurpose
hey syncnixos-rebuild boot
hey sync build-vmBuild a test VM
hey checkComprehensive validation (syntax, flake, eval)
hey pullUpdate flake inputs
hey gcGarbage collection
hey hook <name>Trigger event hooks
hey .<script>Execute bin/ script (WM-aware resolution)
`hey vars get\set`
hey replJanet/Nix REPL

WM-agnostic scripts go in bin/. Use hey.wm.name or hey.wm.mode to adapt behavior per compositor (Hyprland, Niri, BSPWM). Common shared hooks (idle.zsh, battery.zsh, gamemode.zsh) live in bin/hooks/; WM-specific overrides go in config/$WM/hooks/ or hosts/$HOST/hooks/.

Janet Toolchain Style

  • Prefer native Janet features (&opt, &named, &keys, |()) over complex macros.
  • Use try/catch for error handling — never swallow errors by redirecting stderr to /dev/null.
  • Use defcmd macro (in lib/hey/cmd.janet) to define subcommands with automatic option parsing and validation.
  • Mock filesystem interactions with hey/mock.d; never run destructive tests on the host.

Verification & Development Loop

The verification tool is hey check. CRITICAL AI INSTRUCTION: If you have terminal execution capabilities, run hey check syntax first. DO NOT run hey check eval or hey check all automatically, as bad Nix code may cause infinite recursion and permanently hang the terminal. Instead, ask me (the user) to run hey check eval and report the results back to you.

hey check all                         # syntax + flake + eval for current host (default)
hey check syntax                      # parse all .nix files
hey check flake                       # nix flake check
hey check eval --host <name>          # deep evaluation, catches infinite recursion
hey check eval --all-hosts            # full repo health check
hey check eval --show-trace           # bisect dependency chains

Other useful checks:

nix eval .#nixosConfigurations.<host>.config.system.build.toplevel
nix flake check
nix-instantiate --parse <file>

Janet Tests (Not implemented yet, don’t try)

jpm test           # or: judge test/hey

Add a test file in test/hey/ for every new library function or complex logic. Use VMs for integration tests that touch system state (hey build vm --host <name>).

Recursion Safety Rules

  • Avoid referencing config.modules.foo.enable inside a mkIf that defines config.modules.foo.
  • Ensure mkIf conditions never depend on the values they conditionally define.
  • Use --show-trace to bisect circular dependency chains.

Development Loop (Don’t trigger too much, only when updating .nix files)

  1. Modify — apply surgical changes to modules or hosts.
  2. Format — run alejandra <file> on any .nix files you modified to ensure consistent formatting.
  3. Verify — run hey check syntax immediately (if terminal access is available).
  4. Debug — if errors appear, use --show-trace; fix the logic.
  5. Iterate — re-verify until hey check syntax passes. If hey check itself is flawed, fix the toolchain.
  6. Ask User to Run hey check eval: Request the user to execute the deep evaluation check and report results.
  7. Deliver — only proceed to hey sync after a clean evaluation from the user.

A change is not complete until verification is attempted. If a check cannot run, say so explicitly.

Toolchain Verification Protocol

If you modify the core hey toolchain (lib/hey/**, bin/hey, or bin/hey.d/**):

  1. Rebuild: You MUST run hey build hey or ./scripts/build_hey.zsh to apply changes to the compiled binary.
  2. Test Functional Flow: Verify that your changes did not break the hey + Zsh + Rofi interaction.
  3. Test Suites:
    • Zsh Interaction: Run a built-in script (e.g., hey .open-term).
    • Rofi Interaction: Test both Janet and Zsh menus (e.g., hey @rofi audiomenu, hey @rofi powermenu).
    • Launcher Entries: Verify the exec strings defined in modules/desktop/apps/rofi.nix still work.
  4. Iteration: If any test fails (e.g., compilation error, missing symbols, or no UI response), refactor the relevant Janet/Zsh code, rebuild, and re-test.

Refactoring Guidelines

Pre-Refactor “Read” Mandate

Before refactoring or calling a local library function, you MUST read its source code to understand its signature:

  • If working with hey commands, read lib/hey/cmd.janet and lib/hey/sys.janet first.
  • If working with modules, read lib/options.nix and lib/pkgs.nix first.
  • Read docs/ai-language-idioms.md to ensure your code aligns with repo idioms. Explain the function signature back to me in your “Thinking” process before you write the implementation.

Execution Steps

  1. Identify duplicated or host-specific logic.
  2. Extract a shared primitive into lib/, modules/, or bin/.
  3. Update all affected hosts/call sites to consume the shared abstraction (convert at least one real caller in the same change).
  4. Run hey check syntax — fix code until it passes; if hey is broken, fix the toolchain.
  5. Update docs (docs/*.md, README.md, this file) if architecture or workflow changed.
  6. Commit as one coherent slice with a conventional prefix (feat, fix, refactor, docs, chore).

Choosing the Right Layer

WhereWhat goes there
lib/*.nixPure Nix helpers, module-loading utilities
lib/pkgs.nixPackage wrappers and package-oriented helpers
lib/hey/*.janethey runtime helpers
lib/zsh/*Shell helper functions for scripts
bin/*.zshExecutable user-facing commands
modules/Reusable feature modules
hosts/<name>/Host-specific values only

Always extend an existing file if the concept already lives there. Add a helper only after finding at least one concrete caller.

Toolchain Refactoring & Verification

If you modify the core hey toolchain (lib/hey/**, bin/hey, or bin/hey.d/**):

  1. Rebuild: You MUST run hey build hey or ./scripts/build_hey.zsh to apply changes to the compiled binary.
  2. Test Functional Flow: Verify that your changes did not break the hey + Zsh + Rofi interaction.
  3. Test Suites:
    • Zsh Interaction: Run a built-in script (e.g., hey .open-term).
    • Rofi Interaction: Test both Janet and Zsh menus (e.g., hey @rofi audiomenu, hey @rofi powermenu).
    • Launcher Entries: Verify the exec strings defined in modules/desktop/apps/rofi.nix still work.
  4. Iteration: If any test fails (e.g., compilation error, missing symbols, or no UI response), refactor the relevant Janet/Zsh code, rebuild, and re-test.

Anti-patterns to Avoid

  • Editing many unrelated hosts during a library refactor unless required.
  • Introducing an abstraction without converting at least one real caller.
  • Writing docs that describe architecture not present in code.
  • Silently changing operator workflow without updating docs.
  • Putting host-specific values in shared modules.
  • Duplicating profile logic — use modules.profiles.* selectors.
  • Bypassing home.file — use the bridge aliases.
  • Hardcoding paths — use hey.dir, config.home.fakeDir, config.user.home, etc.
  • Adding browser-only settings to Thunderbird (it’s an email client).
  • Redundant Persistence: Never add subdirectories of ~/.local/share, ~/.local/state, ~/.local/user, or ~/.local/bin into home.persistence. These parent directories are already persisted globally in modules/persist.nix.
  • Auto-committing partial work that has not been verified.

Documentation Rules

Update docs in the same commit when modifying:

  • Architecture or module boundaries
  • hey workflow or operator commands
  • Security posture, networking, or service topology
  • Installation, sync, build, or debugging procedures

Target document by scope:

FileScope
README.mdRepository overview and navigation
this file (AGENTS.md)Agent execution policy
docs/*.mdSubsystem-specific design and operator guidance
docs/ai-language-idioms.mdLanguage-specific traps and syntax guides for AI
CHANGELOG.mdAll significant additions, changes, and removals

Do not add aspirational claims unless the code already supports them.


Git & Commit Policy

Use git history as a model: small commits, conventional prefixes, one architectural idea per commit.

  • CRITICAL: Never execute hey sync, or any command requiring sudo automatically. You must stop and instruct me to run these commands.
  • Do not auto-commit by default after every edit.
  • Do commit automatically only when the user explicitly signals it (e.g. “commit this”, “make a commit”, “end-to-end”).
  • SSH-signed commits are fully automated — no YubiKey or manual touch needed.

Commit message pattern: <prefix>(<area>): <what changed>

Before committing:

  • Run git diff --stat and show the diff summary.
  • Ask the user to confirm the changes look correct before staging.
  • Do not assume who made which changes — treat all unstaged diffs as requiring user confirmation.

Default Agent Workflow

  1. Read this file, docs/ai-language-idioms.md, and the relevant docs/ slice before proposing changes.
  2. Read the source of local functions (e.g., in lib/) you plan to call.
  3. Search the web for official Nix or Janet docs if unsure about standard library usage.
  4. Inspect the local architecture before introducing abstractions.
  5. Make the smallest useful change that removes duplication or improves structure.
  6. Run hey check syntax — iterate until it passes.
  7. Update docs if user-facing or architectural behavior changed.
  8. Update CHANGELOG.md with significant changes.
  9. Summarize risks, checks run, and any follow-up work.
  10. Ask user to run hey check eval and report the results.
  11. Commit only when requested.

Key Subsystems (docs/)

DocWhat it covers
editors.mdMulti-editor setup, VS Code extensions, Neovim LSP/DAP, AI agents, pipeline support
software.mdLibreWolf hardening, Spotify theming, fake-home jailing
hardware.mdNVIDIA legacy pinning, CPU microarch, peripherals
packages.mdPackage hierarchy, hey.packages, overlays
desktop.mdMulti-desktop strategy, nested debugging
themes.mdDecoration pattern, app theming, compositor support
nix-expressions.mdmkOpt, mapModules, wrapFakeHome, mkAliasDefinitions, ensures
toolchain.mdhey CLI, defcmd, hooks, WM detection, binscripts
networking-vpn.mdTailscale, Headscale, MagicDNS, DERP
networking-proxy.mdSing-box, SSH tunnels, traffic routing
power-management.mdUniversal role-aware sleep, caffeine, hardware idle
security.mdLanzaboote, LUKS2+FIDO2, YubiKey, agenix, AppArmor
sso-identity.mdKanidm, OAuth2 Proxy, Nginx auth_request
web-services.mdNginx QUIC/HTTP3, PostgreSQL ensures, ACME
storage-disko.mdDisko, Btrfs+LUKS2, impermanence, btrbk
containers-virt.mdPodman, Docker, libvirt, MicroVM
ai-ml.mdOllama, CUDA/ROCm, JupyterHub
data-science.mdUnified Python/R suites, Positron, Templates, Reproducibility
sbc-opi5p.mdOrange Pi 5 Plus setup, RK3588 kernel, edk2 UEFI, cross-compilation, installation
security-hardening.md7-layer defense-in-depth: YubiKey, Lanzaboote, LUKS/FIDO2, kernel hardening, AppArmor, bubblewrap, firewall
ssh.mdSSH config layers, agent vs IdentityFile, MaxAuthTries pitfall, agenix host keys