This file contains guidelines for agentic coding agents working on this NixOS flake configuration repository.
nix build .#nixosConfigurations.<hostname>.config.system.build.toplevel- Build a specific host configurationnix build .#x86_64-linux.myconfig-iso- Build ISO imagenix flake check- Validate all flake outputs across all systemsnix develop --impure- Enter development environment (requires --impure flag)
./build-pkg-for-host.sh <pkg-name> [<hostname>]- Build a single package as it is configured inhome.packagesfor usermhuberon the given host. Hostname defaults to the current machine's hostname. Useful for iterating on bubblewrap/jail wrappers and other module-generated wrappers without rebuilding the whole system or home-manager closure. Example:./build-pkg-for-host.sh jailed-pi f13
./nixfmtall.sh- Format all Nix files (runsnix fmt)./nixfmtall.sh --check- Check Nix formatting without making changes- For shell scripts:
shfmt -d -s -i 4 -ci <file>- Check shell script formatting (4 space indent) shellcheck -x <file>- Lint shell scripts
The pre-commit-check git-hook only enables nixfmt-rfc-style (Nix
formatting). shfmt and shellcheck are commented out in the git-hook block;
they instead run via a separate shell-fmt-check check derivation, scoped to an
explicit files list in flake.nix (switch.sh plus the executed
myconfig.ai.microvm harnesses and the real-KVM validation suite). typos is
not enabled.
Run manually: nix run .#checks.x86_64-linux.pre-commit-check
The CI workflow runs these checks:
nix flake check./nixfmtall.sh --check- Dry-run builds for hosts: f13, workstation, nas, vserver
Build log files for each host are stored in the parent directory:
- Use symlink:
../result.<hostname>.log(e.g.,../result.f13.log) - Direct access:
../_logs/YYYY-MM-DD-myconfig-<hostname>.log - The logs directory
../_logs/contains historical build logs for all hosts
When refactoring a module that should be behavior-preserving (no observable changes to evaluated config), capture a snapshot of the relevant slice of the evaluated NixOS configuration before making changes, then diff against the same query after. A byte-identical diff is strong evidence that the refactor did not alter behavior.
- Identify which hosts actually exercise the module being refactored
(grep for the option / import path, e.g.
myconfig.ai.llama-cpp). - Pick the smallest slice of
configthat captures the module's outputs. Common targets:- The service config it produces, e.g.
config.services.<name>.settings - Generated
home.packagesnames and outPaths myconfig.ai.localModelsor similar registries it contributes to- The full toplevel drv hash (coarse but exhaustive — see below)
- The service config it produces, e.g.
- Save the baseline JSON to
/tmp/opencode/<task>/before-<host>.json. - Perform the refactor (split files, rename helpers, etc.).
git addthe new files —nixevaluates from the git tree, so untracked files are invisible. Forgetting this produces misleading "file does not exist" errors.- Re-run the same
nix evalintoafter-<host>.json. diff before-<host>.json after-<host>.json→ must be empty.
For module-specific config + generated home-manager wrappers:
mkdir -p /tmp/opencode/<task>
nix eval --impure --raw --expr '
let
flake = builtins.getFlake ("git+file://" + toString /home/mhuber/myconfig/myconfig);
cfg = flake.nixosConfigurations."<hostname>";
# --- pick the slices that matter for the module under refactor ---
serviceSettings = cfg.config.services.<name>.settings;
hmPkgs = cfg.config.home-manager.users.mhuber.home.packages;
relevantPkgs = builtins.filter
(p: let n = p.name or p.pname or ""; in
builtins.match "<regex-of-generated-pkg-names>.*" n != null)
hmPkgs;
# Strip non-JSON-serialisable fields (functions, derivations) from
# nested attrsets before toJSON. Keep stable identifying fields.
sanitize = x: { inherit (x) name port; models = x.models or []; };
in
builtins.toJSON {
settings = serviceSettings;
pkgNames = map (p: p.name or p.pname) relevantPkgs;
pkgOutPaths = map (p: p.outPath) relevantPkgs;
# add more slices as needed
}
' > /tmp/opencode/<task>/before-<hostname>.json 2> /tmp/opencode/<task>/before-<hostname>.errAfter refactoring + git add, re-run with after-<hostname>.json and:
diff /tmp/opencode/<task>/before-<hostname>.json \
/tmp/opencode/<task>/after-<hostname>.json \
&& echo IDENTICALFor a single-line "did anything change at all?" check, compare the system toplevel derivation path. If it matches, nothing about the host changed:
nix eval --raw .#nixosConfigurations.<hostname>.config.system.build.toplevel.drvPathThis is the strongest possible check but gives no signal about what diverged when it does change — use the JSON snapshot to localise diffs.
- Untracked files:
nixreads the git tree (dirty or clean), sogit addevery new file before re-evaluating, otherwise the new modules are silently invisible and the "after" eval still uses the old layout or errors with "path does not exist". - Non-serialisable values:
builtins.toJSONwill fail on functions or derivations nested inside attrsets. Strip them via asanitizehelper (keep only the stable identifying fields likename/port/outPath). - Eval warnings vs errors: a non-zero exit + zero-byte output JSON means
the eval failed — read
*.err. Pure warnings (e.g. deprecated options) appear on stderr but exit 0 and produce valid JSON; that's fine. - Latent bugs: if the original code has a bug (e.g. writes
argsbut consumers readparams), the snapshot will encode that buggy behavior. Preserve it verbatim in the refactor for a clean diff, and leave aNOTEcomment pointing at the bug for a follow-up commit. - Closure-equal output paths: when extracting a helper that builds a
derivation, make sure the inputs are identical — even reordering
runtimeInputsor changing whitespace inside awriteShellApplicationtext changes the outPath and breaks the diff.
- Always add newly created files to git after they are created and validated
- Run
git add <filepath>for each new file (e.g.,git add hosts/host.newmachine/default.nix) - For new hosts, add the entire directory:
git add hosts/host.<hostname>/ - Update
flake.nixand metadata files should also be staged - This ensures all changes are tracked and visible via
git status
- Run
./nixfmtall.shto format all Nix files - Run
nix flake checkto validate configuration - Review staged changes with
git diff --staged - Only commit when explicitly requested by the user
- Create commits if you are on a worktree or a feature branch that matches the current task. In that case, committing finished and validated work is expected.
- Do not automatically commit on
master/main. On those branches, leave the changes in the working tree and let the user decide. - Check the current branch first (e.g.
git branch --show-current) before deciding whether to commit.
-
Formatting: Use nixfmt-rfc-style (RFC 51 style)
-
File naming: a module file's name mirrors the top-level option it defines or configures. Use dot-separated names following the option hierarchy.
myconfig.agentUsers.nix→ definesoptions.myconfig.agentUsersmyconfig.secrets.nix→ definesoptions.myconfig.secretsboot.initrd.supportedFilesystems.nix→ configuresboot.initrd.supportedFilesystemsdev.haskell/default.nix→ a subfeature grouped in a directory
-
Module structure: Follow standard NixOS module pattern
{ config, lib, pkgs, ... }: let cfg = config.myconfig.<feature>; in { options.myconfig.<feature> = with lib; { enable = mkEnableOption "myconfig.<feature>"; }; config = lib.mkIf cfg.enable { # Configuration here }; }
-
Imports: For modular features, use
imports = [ ./submodule.nix ];pattern -
Conditionals: Use
lib.mkIffor conditional configuration -
Options: Always define options before config section with
mkEnableOptionfor booleans
- Formatting: 4-space indentation, simplified formatting, case indentation
- Shebang:
#!/usr/bin/env bash - Error handling:
set -euo pipefailat script start - Shellcheck directives: Add
# shellcheck disable=SC<code>for necessary exceptions - Dependencies: Use nix-shell shebang for reproducible:
#! nix-shell -i bash -p <packages>
- flake.nix: Contains outputs defined per system using
eachDefaultSystem - Nixpkgs overlays: Use overlays for custom packages or version pinning
- Modules organization: Group by feature (e.g.,
myconfig.desktop.*,dev.*,services.*) - Host configurations: Use
nixosConfigurationsGen.host-<name>pattern for code reuse - Metadata: Host metadata stored in
hosts/metadata.json
- Always import Nixpkgs from inputs:
inputs.nixpkgs.legacyPackages.${system} - Use
inherit (inputs.nixpkgs) libfor nixpkgs lib functions - Follow flake input convention: define all inputs at top of flake.nix
Local checkouts of upstream sources are available for reference only (e.g. searching for module options, package definitions, or library functions). These directories are not used by the build system — all dependencies are managed via flake inputs in `flake.nix:
- nixpkgs:
~/myconfig/nixos/nixpkgs - home-manager:
~/myconfig/nixos/home-manager
Use these checkouts with Grep/Glob/Read tools to investigate upstream behavior, find existing modules, or look up option definitions. Do not modify these directories — they are read-only references.
To inspect the exact pinned version the build actually consumes (as opposed
to the local checkouts above, which are not used by the build), use the
./get_input.sh helper. It prints the store path of the locked revision of any
top-level flake input from flake.lock:
./get_input.sh nixpkgs # /nix/store/...-source
./get_input.sh home # home-manager input (named `home` in this flake)Then nix store cat, nix run, or ls the returned path to read the pinned
source tree of nixpkgs or home-manager. Inputs that follows another and
non-flake (flake = false) inputs are supported.
- Module files: the file name is the option path it defines or
configures (dropping the value), e.g. a module defining
options.myconfig.agentUserslives atmodules/myconfig.agentUsers.nix; one configuringboot.initrd.supportedFilesystemslives atmodules/boot.initrd.supportedFilesystems.nix. Use a directory (subdir/default.nix) when a feature spans multiple files. All*.nixdirectly undermodules/are auto-imported bynixosModules.coreviabuiltins.readDir, so only the file name matters — there is no import list to update. - Options:
myconfig.<category>.<feature>.enable - Context variables:
cfgfor current config,self/superfor overlays - Host names: lowercase alphanumeric (e.g., f13, workstation, nas)
- Use agenix for secrets management
- Never commit secrets to repository
- Git used for sensitive files (via git-crypt, git-secrets)
- All secrets must be stored in the separate
../priv/repository, never in this repo
- Nix: Use
lib.mkIffor conditional logic rather than throwing errors - Shell: Use
set -euo pipefailfor robust error handling - Build failures always logged and surfaced
Consider adding copyright headers to new files:
# Copyright <year> Maximilian Huber <oss@maximilian-huber.de>
# SPDX-License-Identifier: MIT