These are instructions for LLMs to write code more to my liking. For general project layout, design, and desires, see ./README.org.
When authoring new content, make them org-mode files.
Use :exports code if you just want to show off the code, and don’t want it to
actually run anything.
No file lists in documentation.
Comments must be complete sentences, wrapped at ~80 columns, with standard punctuation. Use two spaces after sentence-ending punctuation for easier sentence matching.
Only comment non-obvious intent, invariants, tradeoffs, or historical constraints; no restating of control flow.
Use Pascal Initialisms (Url) instead of Preserved Initialisms (URL) when
authoring new identifiers in camel case.
The local network uses the .proton domain suffix. Host names declared in
nixos-modules/facts.nix are reachable at <hostname>.proton (e.g.
rubidium.proton, scandium.proton). When SSH-ing to a host by short name,
append .proton — e.g. ssh rubidium.proton.
Try to keep Flake inputs in lexicographical order.
When adding an new input, you don’t need to include it in the outputs section
unless it’s specifically required in that section. This is afforded due to
flake-inputs being aggregated and passed into the hosts using specialArgs.
There are both <system-type>-configs and <system-type>-modules, where
<system-type> is one of nixos, darwin, or home. This corresponds with
whether or not it belongs to NixOS, macOS, or Home Manager.
configs are inert sets of configuration. They could use functions to
calculate or compose settings, but the act of importing the config is what
activates said config. These files do not include config or options
attributes.
modules are configurable option sets. They use config and options
attributes. Importing one of these simply makes the options available, and by
itself doesn’t change anything for a host’s configuration.
I do have some inconsistencies wherein some nixos-modules are actually
nixos-configs, but I didn’t have the split pattern at time of authorship of
those.
A nix build '.#nixosConfigurations.${host}.config.system.build.toplevel'
invocation is almost assured to fail because there will likely be a mismatch
between architecture and platform. See deploy tools for how to do a deployment,
which will also do your build.
Scripts that ship as part of a derivation are co-located with their
default.nix in a subdirectory under derivations/, following the same
convention as nixpkgs. The directory name, the script file name, and the
derivation’s name (or pname) must all match.
The script file carries a shebang appropriate to its language so that editors
recognise it and it can be executed directly outside of Nix (given the right
interpreter and dependencies on PATH). Use the appropriate writers function
for the script’s language — no custom installPhase needed. Each writer
handles the shebang, installs the binary, and runs a linter at build time
(shellcheck for shell, pyflakes for Python): writeShellApplication for
shell scripts, writers.writePython3Bin for Python.
derivations/
my-tool/
my-tool <- script with shebang; runnable directly
default.nix <- derivation; patches shebang, wires runtime deps
The older pattern of a bare .nix file in derivations/ reading its script
from ../scripts/<name> still exists for historical scripts, but new scripts
should use the co-located layout above.
Secrets are managed with agenix-rekey. Only specify rekeyFile when there is
a strong reason to control the output path (e.g. a secret that must live
alongside a specific config file). For generated secrets, omit rekeyFile
entirely — agenix-rekey will place them under secrets/generated/
automatically, which makes it easy to identify which secrets are generated vs.
manually managed.
Bind agenix secrets into systemd units using LoadCredential rather than
setting owner on the secret. Systemd reads the source file as root and
exposes it under /run/credentials/<unit-name>/<credential-name>, readable
only by the service — no ownership tuning required. This is also the only
correct approach when the service uses DynamicUser = true, where no stable
uid exists to assign ownership to.
The consuming application config (e.g. password_file, credentials_file)
must reference the /run/credentials/<unit-name>/<credential-name> path, not
the raw agenix path.
age.secrets.my-secret = { }; # no owner needed
systemd.services.my-service.serviceConfig.LoadCredential = [
"my-secret:${config.age.secrets.my-secret.path}"
];