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 parts 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/, 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, Firefox, 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.
Pure & Isolated System Closures (No Target Architecture Leakage)
- Pure Closures: When modifying or refactoring shared library code (such as
lib/nixos.nix,lib/pkgs.nix, etc.), you must ensure that each host’s evaluated system configuration remains strictly isolated and untainted by other architectures. - No Target Leakage: If evaluating a host targeting
x86_64-linux(e.g.lab-matrix), there must be absolutely noaarch64package or builder paths in the output derivation. Conversely,aarch64-linuxtargets (e.g.sbc-opi5p) must evaluate cleanly as pure cross-compiled (or native) closures without leaking native host system packages that do not compile on the target platform. - Dynamic Package Binding: Always construct library helpers (e.g. wrappers, menu generators, environment builders) using the target host’s specific
pkgs(bound dynamically during NixOS configuration evaluation), rather than utilizing the global staticpkgsdefined at flake load time. - Conditional Platforms: If a package or system tool has platform-specific compatibility restrictions in Nixpkgs (e.g.
android-studioorarduino-idewhich only targetx86_64-linux), always gate its inclusion in system or user packages usinglib.optionals pkgs.stdenv.hostPlatform.is<arch>(e.g.,lib.optionals pkgs.stdenv.hostPlatform.isx86_64) rather than adding it unconditionally.
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; }
];
Adding a PostgreSQL-backed service
When creating a new service module that needs its own database, use the
project’s PostgreSQL wrapper (modules/services/database/postgresql) —
never services.postgresql directly:
modules.services.database.postgresql = {
enable = true;
# Extensions this service needs (pgvector, vectorchord, etc.)
extraExtensions = ps: [ps.pgvector];
# postgresql.conf overrides merged on top of module defaults
settings = {
shared_preload_libraries = ["vchord.so"];
search_path = "\"$user\", public, vectors";
};
ensures = [
{
username = "myapp";
database = "myapp";
passwordFile = config.age.secrets.myapp-postgresql-password.path;
}
];
};
Key rules:
enable = trueis idempotent — multiple services can set it, the DB starts once.extraExtensionsis a functionps: [...]— it receives thepkgs.postgresql_18extension set, not raw nixpkgs. Useps.pgvector,ps.vectorchord, etc.settingsis merged viamkMergeintoservices.postgresql.settings. Use it forshared_preload_libraries,search_path, or anypostgresql.confkey. No need to wrap values in lists —mkMergehandles list concatenation.ensureshandles user + database creation. The post-start script also sets passwords from agenix secrets whenpasswordFileis provided.- The wrapper pins PostgreSQL 18 —
stateVersiondoes NOT affect the version. If you need a different version, overridepackage.
Database State Operations
ensures handles user/role and database creation. It does not manage in-database state — user accounts, application rows, schema migrations involving data. When debugging or setting up a service that stores state in PostgreSQL:
- Never run destructive queries (
DROP,TRUNCATE,DELETE) without explicit user approval. - Always show the exact SQL before running it. Use
sudo -u postgres psql -d <db> -c '<query>'— full CLI, no shorthand. - Prefer read-only queries first (
SELECT,\dt,\du) to understand the current state. - For user-account operations (Atuin registration, app user creation, password resets): check if the application provides a CLI/API first. Direct SQL inserts are a last resort — app-level hashing and foreign-key constraints make manual inserts error-prone.
- Temporary registration patterns: Some services (e.g., Atuin) gate user creation behind a config toggle (
openRegistration). The Nix-idiomatic approach: enable the toggle, deploy, register the user through the application, disable the toggle, deploy again. Document the two-deploy cycle in the service’s module comment and in docs.
Ask me to approve any database mutation before executing it. Read-only queries are always fine.
Firewall Engineering Pattern (Dual-Stack LAN-Restriction)
To protect the homelab from public WAN exposure, we enforce a strict, consistent firewall engineering pattern across all modules.
The Rules:
- Gateways (Globally Open): Only public-facing edge gateways — Nginx (
modules/services/web/nginx.nix) and Tailscale (modules/profiles/network/ts0.nix) — are permitted to setopenFirewall = trueor open global TCP/UDP ports. - Internal Services (LAN/Tailscale Only): Every other service (e.g. SSH, Samba, Ollama, Jellyfin, Sunshine, Syncthing) MUST keep the upstream package’s firewall option disabled (
openFirewall = false;explicitly). - Firewall Helpers: Restrict inbound access strictly to Tailscale + trusted LAN using
networking.firewall = mkFirewallFor { ... };fromlib/firewall.nix.
Standard Firewall Configuration Template:
services.myservice = {
enable = true;
openFirewall = false; # Secure by default, LAN-restricted below
};
# Restrict incoming connections to Tailscale + trusted LAN ranges
networking.firewall = mkIf cfg.openFirewall (
hey.lib.firewall.mkFirewallFor {
tcp = [ "8080" ];
comment = "myservice";
}
);
Key Details:
- All helpers (
mkFirewallFor,mkRestrictedPort) generate dual-stack rules, applying rules to bothiptables(IPv4lanSources) andip6tables(IPv6lanSourcesV6). - Traffic arriving on
tailscale0completely bypasses the firewall viatrustedInterfaces, meaning Tailscale connections succeed seamlessly even whenopenFirewall = false. - Always use colon syntax
137:138for native range queries intcporudparrays, or usetcpRange/udpRangehelpers.
Firewall Engineering Pattern (Dual-Stack LAN-Restriction)
To protect the homelab from public WAN exposure, we enforce a strict, consistent firewall engineering pattern across all modules.
The Rules:
- Gateways (Globally Open): Only public-facing edge gateways — Nginx (
modules/services/web/nginx.nix) and Tailscale (modules/profiles/network/ts0.nix) — are permitted to setopenFirewall = trueor open global TCP/UDP ports. - Internal Services (LAN/Tailscale Only): Every other service (e.g. SSH, Samba, Ollama, Jellyfin, Sunshine, Syncthing) MUST keep the upstream package’s firewall option disabled (
openFirewall = false;explicitly). - Firewall Helpers: Restrict inbound access strictly to Tailscale + trusted LAN using
networking.firewall = mkFirewallFor { ... };fromlib/firewall.nix.
Standard Firewall Configuration Template:
services.myservice = {
enable = true;
openFirewall = false; # Secure by default, LAN-restricted below
};
# Restrict incoming connections to Tailscale + trusted LAN ranges
networking.firewall = mkIf cfg.openFirewall (
hey.lib.firewall.mkFirewallFor {
tcp = [ "8080" ];
comment = "myservice";
}
);
Key Details:
- All helpers (
mkFirewallFor,mkRestrictedPort) generate dual-stack rules, applying rules to bothiptables(IPv4lanSources) andip6tables(IPv6lanSourcesV6). - Traffic arriving on
tailscale0completely bypasses the firewall viatrustedInterfaces, meaning Tailscale connections succeed seamlessly even whenopenFirewall = false. - Always use colon syntax
137:138for native range queries intcporudparrays, or usetcpRange/udpRangehelpers.
Nginx Reverse Proxy Pattern — Service Module Template
When creating a new web service module behind nginx, follow this pattern.
Deviating from it causes nginx -t failures at deploy time.
Library Helpers (lib/nginx.nix)
Two-tier vhost generators that replace ~15 lines of boilerplate:
| Helper | When to use |
|---|---|
genNginxVhost | Standard reverse-proxy service with a "/" proxyPass |
genNginxVhostBase | Service that manages its own locations (PHP-FPM, fcgiwrap, custom logic) |
Standard Service Module Template
# modules/services/<area>/<name>.nix
{ hey, lib, config, pkgs, ... }:
with lib;
with hey.lib; let
cfg = config.modules.services.<area>.<name>;
baseDomain = config.modules.services.baseDomain;
in {
options.modules.services.<area>.<name> = {
enable = mkBoolOpt false;
domain = mkOpt types.str "<name>.${baseDomain}";
port = mkOpt types.port <pick-unused-port>;
nginx = {
enable = mkBoolOpt true;
ssl = mkBoolOpt false; # false on L2 hosts (lab-matrix), true on L1 (vps-pacman)
};
oauth2 = {
enable = mkBoolOpt false;
allowedGroups = mkOpt (types.listOf types.str) [];
};
};
config = mkIf cfg.enable {
services.nginx.virtualHosts = mkIf cfg.nginx.enable {
"${cfg.domain}" = genNginxVhost {
ssl = cfg.nginx.ssl;
proxyPass = "http://127.0.0.1:${toString cfg.port}";
proxyWebsockets = true; # for WebSocket support
quic = config.modules.services.web.nginx.quic;
kTLS = config.modules.services.web.nginx.kTLS;
oauth2 = cfg.oauth2.enable;
oauth2Groups = cfg.oauth2.allowedGroups;
};
};
};
}
Critical Rules — Read Before Adding extraConfig
The nixpkgs nginx module emits these directives from proxyWebsockets = true
at the location level:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
And from recommendedProxySettings = true (global, HTTP level):
proxy_redirect off;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_http_version 1.1; # HTTP-level default
proxy_set_header "Connection" "";
include recommendedProxyConfig; # Host, X-Real-IP, X-Forwarded-For/Proto/Host/Server
NEVER add these to extraConfig: proxy_http_version, proxy_set_header Upgrade, proxy_set_header Connection, or any header from
recommendedProxyConfig. Doing so duplicates what nixpkgs already emits and
breaks nginx -t with "<directive>" directive is duplicate.
For WebSocket locations that need custom headers beyond what proxyWebsockets
provides (e.g., tcp_nodelay on, proxy_set_header Host $host), add ONLY
those — never the three WebSocket headers.
Location emission order (nixpkgs default.nix lines 510-548):
1. proxy_pass
2. proxyWebsockets → Upgrade/Connection headers
3. extraConfig ← your additions go here
4. include recommendedProxyConfig
Check existing config:
nginx -T 2>/dev/null | grep -n 'proxy_http_version\|proxy_set_header Upgrade\|proxy_set_header Connection'
For full details, see docs/web-services.md § Nixpkgs Nginx Module Internals.
Host Composition
Each host in hosts/ has:
hosts/<name>/
default.nix imports + system arch
modules/
modules.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, profile) |
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
hey check profile --host <name> # eval flamegraph profiling (Nix 2.30+)
hey check profile --flamegraph --open # profile + generate SVG + open in browser
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
hey is built via a two-tier system:
- Production:
packages/hey/default.nix— a hermetic Nix derivation built duringnixos-rebuild. The compiled binary and all Janet modules live in the nix store and are copied to every host as part of the system closure. No activation-time compilation needed. - Development:
scripts/build_hey.zsh(orhey build hey) — compiles into$JANET_TREE/bin/(~/.local/share/janet/jpm_tree/bin/). Because PATH lists$JANET_TREE/binbefore the nix-storehey, the locally-built version takes priority during development.
If you modify the core hey toolchain (lib/hey/**, bin/hey, or bin/hey.d/**):
- Rebuild: You MUST run
./scripts/build_hey.zsh(orhey build hey) to apply changes to the compiled binary. - Test Dev Binary: Verify your changes work at the dev level.
- 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.,
- Test System Binary (critical for
lib/hey/**changes): The dev build runs from source with real env vars and will not reproduce Janet image creation bugs. Always verify with the hermetic Nix build:
This builds the exact binary thatnix build .#hey && result/bin/hey info && result/bin/hey exec /tmp/test_path.shhey syncdeploys to/run/current-system/sw/bin/hey. Janet evaluates module-leveldef,var, anddelayat image creation time (the Nix build sandbox), capturing build-time env vars (USER=nix-build, temp dirs). Onlydefn(pure functions called at runtime) is safe for values derived fromos/getenvor filesystem reads. Seedocs/ai-language-idioms.md§Janet Image Creation Trap. - 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.
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>
Rules
- Never rebase, amend published commits, or rewrite shared history. If a previous commit was wrong, fix forward with a deliberate follow-up commit — do not silently rewrite it. Rebasing destroys the real development record and breaks any branches based on those commits.
- Each commit must be a single, correct, self-contained change. Do not create chains of incremental
fix(previous-commit)patches. If you discover the initial implementation was incomplete, step back and design the correct solution before committing. A commit should never need its successor to “fix” it. - Update this file (
AGENTS.md) in the same commit when architecture or workflow changes. If you change how paths resolve, how the build works, or how modules compose, this file must reflect that reality in the same commit. The agent guide must never drift from the code. - Just change current code. Do not create fix-up commits that patch something you just wrote. If you’re still iterating, the commit hasn’t happened yet — get it right once.
Before committing:
- Run
git diff --statand show the diff summary. - Ask the user to confirm the changes look correct before staging.
Release Process
A GitHub Release is triggered automatically by pushing a version tag — no gh release create needed.
- Workflow:
.github/workflows/release.ymlfires onpush: tags: ['v*']and usessoftprops/action-gh-release@v2to create the release with auto-generated notes and a docs PDF artifact. - How to release:
git push origin dev --tagsafter creating an annotated tag (git tag -a v0.X.0 -m "..."). The CI pipeline handles the rest. - Versioning: Tags follow
v<major>.<minor>.<patch>(e.g.v0.7.0). Increment the minor version for feature batches, patch for hotfixes. - Do not use
gh release create— it’s redundant with the automated workflow.
AI Agent Post-Release Verification:
After pushing a new tag, the agent MUST verify that the release CI workflow
succeeded. If gh is authenticated (gh auth status), use these commands:
# Check the latest release workflow runs (limit 6 covers the recent tag push)
gh run list --workflow=release.yml --limit 6
# Watch a specific run until completion (use the run ID from the list above)
gh run watch <run-id>
# If the run failed, inspect the logs to diagnose
gh run view <run-id> --log-failed
# Verify the release was created and has the PDF artifact
gh release view <tag-name>
If gh is not authenticated, fall back to the GitHub API anonymously:
curl -s 'https://api.github.com/repos/alienzj/dotfiles/actions/workflows/release.yml/runs?per_page=5' \
| python3 -c "import json,sys; [print(f'{r[\"head_branch\"]:12} {r[\"conclusion\"] or \"running\":12}') for r in json.load(sys.stdin)['workflow_runs']]"
If the release workflow failed, the agent must:
- Diagnose the failure from the logs
- Fix the root cause (usually
scripts/build-docs-pdf.shYAML parse errors from---horizontal rules in concatenated markdown files) - Delete the broken GitHub Release (if one was partially created):
gh release delete <tag> --yes - Move the tag to the fix commit and force-push to re-trigger:
git tag -f -a <tag> -m "<tag>: re-release with fix" <commit> git push origin <tag> --force
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.
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.
- Creating chains of
fix(previous-commit)patches. If a change needs multiple follow-up fixes, the initial commit was not ready. Design the correct solution and commit once. - Rebasing to “clean up” history — this rewrites shared history and erases the real development record.
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.
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 |
neovim.md | AstroNvim v6, lazy.nvim plugin management, LSP/DAP, customization |
tmux.md | Tmux session management, theme switching, status bar configuration |
keybindings.md | Cross-WM keybinding system, compositor-specific mapping |
media.md | mpv configuration, ModernZ OSC, keybindings, jellyfin-mpv-shim integration |
shell-history.md | Atuin + FZF + Zsh substring search three-tiered history system |
software.md | Firefox 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 |
noter.md | Note-taking, sketching, and hybrid Zotero syncing |
nix-expressions.md | mkOpt, mapModules, wrapFakeHome, mkAliasDefinitions, ensures |
toolchain.md | hey CLI, defcmd, hooks, WM detection, exec-path, Janet image trap, two-tier dev/system |
ai-language-idioms.md | Nix/Janet/Zsh/matugen traps: infinite recursion, Janet image trap, matugen 4.0.0 filters |
documentation-infra.md | Docs website (Astro), PDF generation (pandoc+typst), release pipeline |
bin-scripts.md | All bin/ scripts and their usage: WM tools, AI/LLM, media, hooks, dev utilities |
path.md | Complete PATH construction, desktop file discovery, layer mechanisms, process-type differences, debugging |
networking-vpn.md | Tailscale, Headscale, MagicDNS, DERP |
networking-ddns.md | Dynamic DNS, public/private/CGNAT IPs, direct-to-home vs VPS paths, tradeoffs |
networking-proxy.md | Sing-box architecture, SSH tunnels, traffic routing |
networking-topology.md | Network interfaces, physical link speeds, and workstation-NAS diagnostics |
networking-firewall.md | Firewall helpers, lanSources, complete port table, decision guide, Docker/Podman caveats |
router.md | ASUS Merlin customizations, opkg/Entware setup, soft router platform analysis, and behavior management |
sing-box.md | Config lifecycle, DNS reference, validation, debugging, error patterns |
power-management.md | Universal role-aware sleep, caffeine, hardware idle |
security.md | Lanzaboote, LUKS2+FIDO2, YubiKey, agenix, AppArmor |
security-auth-logic.md | Auth flow diagrams, permission model, OAuth2/OIDC token handling |
git-signing.md | SSH-signed commits, GPG setup, allowed_signers, commit verification |
vulnerability-response.md | CVE tracking, kernel module blocking, temporary mitigations |
sso-identity.md | Kanidm, OAuth2 Proxy, Nginx auth_request |
web-services.md | Nginx QUIC/HTTP3, PostgreSQL ensures + extensions + upgrades, ACME, DNS/ad-blocking architecture, Glance dashboard |
service-audit.md | Fleet health checks, journal scanning, weekly audit procedures |
systemd-services.md | Custom systemd units, service lifecycle, per-session targets, debugging |
storage-disko.md | Disko, Btrfs+LUKS2, impermanence, btrbk |
persistence.md | Impermanence, persisted directory rules, tmpfs root |
containers-virt.md | Podman, Docker, libvirt, MicroVM |
backup.md | Restic backup workflows, SFTP backend configuration, and repository recovery |
ai-ml.md | Ollama, CUDA/ROCm, JupyterHub |
data-science.md | Unified Python/R suites, Positron, Templates, Reproducibility |
hardware-ai-architectures.md | AI PC, unified memory, Apple Silicon, and high-performance ARM workstation comparison |
sbc-opi5p.md | Orange Pi 5 Plus setup, RK3588 kernel, edk2 UEFI, cross-compilation, installation |
security-hardening.md | 7-layer defense: YubiKey, Lanzaboote, LUKS/FIDO2/TPM2, kernel, AppArmor, bubblewrap, firewall, systemd-lock-handler |
email.md | Stalwart public email: MX/DKIM/DMARC, nginx stream proxy, ACME, outbound relay |
ssh.md | SSH config layers, agent vs IdentityFile, MaxAuthTries pitfall, agenix host keys |
ops.md | Fleet deployment, remote sync, maintenance commands |
workflow.md | Development workflow, debugging recipes, common task patterns |
ai-parallel-workflow.md | Multi-agent background task orchestration |
homelab-bootstrap.md | Fresh homelab setup, Kanidm bootstrap, crash-loop diagnosis, troubleshooting |
refactor-plan.md | Remaining TODOs: BSPWM parity, Niri window metadata, Janet tests |