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, nogetEnv, 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.
- For Janet (
*.janet):- Before writing complex Janet code, use your web search/browser tool to search
site:janet-lang.org/docs <query>or readhttps://janet-lang.org/api/index.html. - Do not assume Clojure or Common Lisp standard libraries apply to Janet.
- Before writing complex Janet code, use your web search/browser tool to search
- For Nix (
*.nix):- If unsure about a Nixpkgs function, search
https://noogle.devorsearch.nixos.org. - If unsure about Nix builtins, search
https://nix.dev/manual/nix/latest/language/builtins.
- If unsure about a Nixpkgs function, search
- For Zsh (
*.zsh):- Ensure scripts are POSIX compliant where possible, but leverage native Zsh arrays and parameter expansion heavily.
- Mandatory Idiom Review:
- Always read
docs/ai-language-idioms.mdbefore executing large refactors in Nix, Janet, or Zsh to avoid common traps (e.g., infinite recursion in Nix, macro abuse in Janet).
- Always read
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
mkMergeto 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
.noloadfile in a directory to skip auto-loading its contents. This is the preferred method when a directory contains internal module parts (likemodules/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
importa 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.themeoptions. - 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:
pkgs.*— Nixpkgs (unstable channel)pkgs.pkgs-stable.*— Nixpkgs 25.11 fallbackhey.packages.*— Localpackages/derivations (always use explicit hashes forfetchFromGitHub)
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:
- Argument
self: You MUST includeselfin the function arguments. ThemkFlakefunction inlib/nixos.nixcallspkgs.callPackagewith{ self = self.packages.${system}; }. - Variadic Arguments: Always include
...in the argument list to prevent “unexpected argument” errors whencallPackagepasses extra context. - Source Fetching: Use explicit hashes (SRI format) for
fetchFromGitHub. Usenix-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):
| Command | Purpose |
|---|---|
hey sync | nixos-rebuild boot |
hey sync build-vm | Build a test VM |
hey check | Comprehensive validation (syntax, flake, eval) |
hey pull | Update flake inputs |
hey gc | Garbage collection |
hey hook <name> | Trigger event hooks |
hey .<script> | Execute bin/ script (WM-aware resolution) |
| `hey vars get\ | set` |
hey repl | Janet/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/catchfor error handling — never swallow errors by redirectingstderrto/dev/null. - Use
defcmdmacro (inlib/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.enableinside amkIfthat definesconfig.modules.foo. - Ensure
mkIfconditions never depend on the values they conditionally define. - Use
--show-traceto bisect circular dependency chains.
Development Loop (Don’t trigger too much, only when updating .nix files)
- Modify — apply surgical changes to modules or hosts.
- Format — run
alejandra <file>on any.nixfiles you modified to ensure consistent formatting. - Verify — run
hey check syntaximmediately (if terminal access is available). - Debug — if errors appear, use
--show-trace; fix the logic. - Iterate — re-verify until
hey check syntaxpasses. Ifhey checkitself is flawed, fix the toolchain. - Ask User to Run
hey check eval: Request the user to execute the deep evaluation check and report results. - Deliver — only proceed to
hey syncafter 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/**):
- Rebuild: You MUST run
hey build heyor./scripts/build_hey.zshto apply changes to the compiled binary. - Test Functional Flow: Verify that your changes did not break the
hey+ Zsh + Rofi interaction. - 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
execstrings defined inmodules/desktop/apps/rofi.nixstill work.
- Zsh Interaction: Run a built-in script (e.g.,
- 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
heycommands, readlib/hey/cmd.janetandlib/hey/sys.janetfirst. - If working with modules, read
lib/options.nixandlib/pkgs.nixfirst. - Read
docs/ai-language-idioms.mdto 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
- Identify duplicated or host-specific logic.
- Extract a shared primitive into
lib/,modules/, orbin/. - Update all affected hosts/call sites to consume the shared abstraction (convert at least one real caller in the same change).
- Run
hey check syntax— fix code until it passes; ifheyis broken, fix the toolchain. - Update docs (
docs/*.md,README.md, this file) if architecture or workflow changed. - Commit as one coherent slice with a conventional prefix (
feat,fix,refactor,docs,chore).
Choosing the Right Layer
| Where | What goes there |
|---|---|
lib/*.nix | Pure Nix helpers, module-loading utilities |
lib/pkgs.nix | Package wrappers and package-oriented helpers |
lib/hey/*.janet | hey runtime helpers |
lib/zsh/* | Shell helper functions for scripts |
bin/*.zsh | Executable 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/**):
- Rebuild: You MUST run
hey build heyor./scripts/build_hey.zshto apply changes to the compiled binary. - Test Functional Flow: Verify that your changes did not break the
hey+ Zsh + Rofi interaction. - 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
execstrings defined inmodules/desktop/apps/rofi.nixstill work.
- Zsh Interaction: Run a built-in script (e.g.,
- 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/binintohome.persistence. These parent directories are already persisted globally inmodules/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
heyworkflow or operator commands- Security posture, networking, or service topology
- Installation, sync, build, or debugging procedures
Target document by scope:
| File | Scope |
|---|---|
README.md | Repository overview and navigation |
this file (AGENTS.md) | Agent execution policy |
docs/*.md | Subsystem-specific design and operator guidance |
docs/ai-language-idioms.md | Language-specific traps and syntax guides for AI |
CHANGELOG.md | All 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 --statand 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
- Read this file,
docs/ai-language-idioms.md, and the relevantdocs/slice before proposing changes. - Read the source of local functions (e.g., in
lib/) you plan to call. - Search the web for official Nix or Janet docs if unsure about standard library usage.
- Inspect the local architecture before introducing abstractions.
- Make the smallest useful change that removes duplication or improves structure.
- Run
hey check syntax— iterate until it passes. - Update docs if user-facing or architectural behavior changed.
- Update
CHANGELOG.mdwith significant changes. - Summarize risks, checks run, and any follow-up work.
- Ask user to run
hey check evaland report the results. - Commit only when requested.
Key Subsystems (docs/)
| Doc | What it covers |
|---|---|
editors.md | Multi-editor setup, VS Code extensions, Neovim LSP/DAP, AI agents, pipeline support |
software.md | LibreWolf hardening, Spotify theming, fake-home jailing |
hardware.md | NVIDIA legacy pinning, CPU microarch, peripherals |
packages.md | Package hierarchy, hey.packages, overlays |
desktop.md | Multi-desktop strategy, nested debugging |
themes.md | Decoration pattern, app theming, compositor support |
nix-expressions.md | mkOpt, mapModules, wrapFakeHome, mkAliasDefinitions, ensures |
toolchain.md | hey CLI, defcmd, hooks, WM detection, binscripts |
networking-vpn.md | Tailscale, Headscale, MagicDNS, DERP |
networking-proxy.md | Sing-box, SSH tunnels, traffic routing |
power-management.md | Universal role-aware sleep, caffeine, hardware idle |
security.md | Lanzaboote, LUKS2+FIDO2, YubiKey, agenix, AppArmor |
sso-identity.md | Kanidm, OAuth2 Proxy, Nginx auth_request |
web-services.md | Nginx QUIC/HTTP3, PostgreSQL ensures, ACME |
storage-disko.md | Disko, Btrfs+LUKS2, impermanence, btrbk |
containers-virt.md | Podman, Docker, libvirt, MicroVM |
ai-ml.md | Ollama, CUDA/ROCm, JupyterHub |
data-science.md | Unified Python/R suites, Positron, Templates, Reproducibility |
sbc-opi5p.md | Orange Pi 5 Plus setup, RK3588 kernel, edk2 UEFI, cross-compilation, installation |
security-hardening.md | 7-layer defense-in-depth: YubiKey, Lanzaboote, LUKS/FIDO2, kernel hardening, AppArmor, bubblewrap, firewall |
ssh.md | SSH config layers, agent vs IdentityFile, MaxAuthTries pitfall, agenix host keys |