Imported from Muddyblack/NixOS (
AGENTS.md). Install upstream withnpx skills add Muddyblack/NixOS. Copyright stays with the author.
NixOS Config Guidelines for AI Assistants
When working with this NixOS configuration, follow these rules strictly. Do not add verbose comments, duplicate packages, or break the established structure.
Commands
CI (.github/workflows/check.yml) runs exactly four gates. Run all four locally before calling
a change done — there is no test suite:
nix flake check --no-build --show-trace
alejandra --check . # alejandra . to actually format
deadnix --fail .
gitleaks git . --redact --no-banner # also a pre-commit hook on staged changes
Everyday commands are shell aliases/functions, not a task runner: upnix/rebuild (deploy),
update (flake update), gen (list generations), clean/gcnix (garbage collect).
Check a change without switching the running system:
nix eval .#nixosConfigurations.muddyblack.config.system.build.toplevel --show-trace # eval only
nixos-rebuild build --flake .#muddyblack # build only
nix build .#nixosConfigurations.muddyblack-lite.config.system.build.vm && ./result/bin/run-muddyblack-lite-vm
deploy.sh is the real deployment surface (bash deploy.sh help for all flags): fresh
(disko partition + install; --remote goes through nixos-anywhere), install, switch
(--offline, --remote <host>, --nh, --profile <host>), sops-setup, secrets-edit,
secrets-rotate. upnix calls deploy.sh switch and plays assets/sounds/{success,error}.wav.
nix develop gives nix, git, sops, age, alejandra, deadnix, gitleaks.
Structure
nixos-config/
├── flake.nix # Main flake - keep minimal
├── deploy.sh # Installation & deployment script
├── assets/ # All images/media/sounds here
│ ├── profile.png
│ ├── boot/ # GRUB & rEFInd backgrounds
│ ├── icons/ # App and NixOS logo icons
│ ├── plymouth/ # Boot splash themes (scanned by deploy.sh)
│ ├── readme/ # Docs screenshots & logos
│ ├── wallpapers/
│ └── sounds/
│ ├── success.wav # Played on successful upnix
│ └── error.wav # Played on failed upnix
├── hosts/
│ ├── <hostname>/
│ │ ├── configuration.nix # System config only
│ │ ├── hardware-configuration.nix
│ │ └── packages.nix # System packages only
│ └── common.nix # Shared system config
├── modules/home-manager/
│ ├── home.nix # Imports only, minimal logic
│ ├── packages.nix # User packages only
│ ├── ai-agents.nix # AI agent CLI catalogue; hosts pick with ai.agents
│ ├── theme.nix # GTK/Kvantum/icon symlinks
│ ├── shell.nix # Zsh/direnv/terminal tools
│ ├── plasma-settings.nix # KDE Plasma config
│ ├── caelestia.nix # Caelestia Shell config
│ ├── aliases.nix # Shell aliases
│ └── functions.nix # Shell functions
├── pkgs/ # Custom packages & overlays
│ ├── default.nix # Overlay definition
│ ├── ai/ # Agent CLIs not in nixpkgs; versions in nvfetcher.toml (update-ai)
│ ├── widgets/ # Custom UI widgets (AGS/QML)
│ └── <package>.nix # One file per package
└── dev-shells/ # Development environment templates
Architecture
Two hosts, one builder. flake.nix defines mkHost and applies it to
hosts/muddyblack/configuration.nix (full desktop) and hosts/muddyblack-lite/configuration.nix
(light / VM). Both get disko, impermanence, nix-flatpak and home-manager (with plasma-manager +
caelestia-shell as sharedModules) plus the ./pkgs overlay. username arrives via specialArgs
— thread it through, never hardcode muddyblack.
Feature flags. Each modules/nixos/features/*.nix declares options.features.<name>.enable
and wraps its config in lib.mkIf. hosts/common.nix enables the always-on ones (impermanence,
flatpak); hosts/muddyblack/configuration.nix enables the rest. New optional system functionality
goes into a feature module with an enable option, never unconditionally into common.nix.
Install-time config is generated. hosts/deploy-config.nix (disk device, dual-boot,
bootloader, plymouth theme, features.desktops.*.enable) is written by deploy.sh, imported via
lib.optional (builtins.pathExists ...), and overrides the lib.mkDefaults in common.nix.
bootloader is deliberately unset in common.nix — its only sources are deploy-config.nix and the
option default in modules/nixos/hardware.nix. There is no hardware-configuration.nix; disko
owns partitioning.
Two nixpkgs channels. Everything tracks nixos-26.05; an explicit inherit (unstablePkgs) ...
list in the flake overlay pulls specific packages (caelestia-*, vscode, zed-editor, tailscale,
typst toolchain, …) from unstable. When a package just needs to be newer, add it to that list
rather than adding a flake input. The overlay also exposes the whole set as pkgs.unstable,
which modules/home-manager/ai-agents.nix uses for its catalogue. Inputs follows nixpkgs/home-manager — the
comments in flake.nix explain each exception, don't "fix" them.
Overlay. pkgs/default.nix takes inputs, grouped widgets / themes / icons-cursors / boot /
apps. Own KDE widgets come from their upstream flakes via upstreamWidget input — never re-derive
them locally, NixOS-specific fixes land upstream. Third-party ones are
callPackage ./widgets/<name>.nix.
Home-manager. home.nix is imports plus mimeapps/desktop-entry glue only; every sibling module
is single-purpose. Plasma (plasma-settings.nix) and Hyprland (hyprland.nix) are both configured
and picked at the login screen — a desktop change usually has to be considered for both.
Impermanence. / is wiped on boot. Anything that must survive belongs in
modules/nixos/features/impermanence.nix (environment.persistence."/persist" for system, the
nested users.<name> block for home). A new stateful service is not done until its state directory
is persisted.
Secrets. sops-nix with secrets/secrets.yaml and age keys in .sops.yaml, gated behind
features.sops.enable so forks build without it. Edit via deploy.sh secrets-edit (alias
secrets).
Rules
Packages
- System packages →
hosts/<hostname>/packages.nix - User packages →
modules/home-manager/packages.nix - NEVER duplicate packages between system and user
git,vim,curl,wget→ system onlyfastfetch,btop,yazi,bat→ user only- KDE/Plasma stuff → user packages
Comments
- Only
# Source: <url>for custom packages - NO verbose section headers like
# ==================== SECTION ==================== - NO AI explanation comments
- NO "The Clean Way" or similar branding
Assets
- Profile pictures →
assets/profile.png - Wallpapers →
assets/wallpapers/ - Bootloader backgrounds →
assets/boot/ - App & logo icons →
assets/icons/ - Docs screenshots →
assets/readme/ - Reference as
../../assets/from modules
Flake
- Keep outputs block compact, no excessive comments
Custom Packages (pkgs/*.nix)
# Source: https://github.com/owner/repo
{ stdenvNoCC, fetchFromGitHub }:
stdenvNoCC.mkDerivation rec {
pname = "package-name";
version = "1.0.0";
# ... rest of derivation
}
- First line:
# Source:URL only - No multi-line header comments
- No inline explanatory comments unless truly necessary
Overlay (pkgs/default.nix)
final: prev: {
package-name = final.callPackage ./package-name.nix { };
}
- One line per package
- No comments in overlay file
Home Manager Modules
- Each module is single-purpose
- Imports go at top of
home.nix - No inline package lists in
home.nix- usepackages.nix
Plasma Global Shortcuts (two traps)
-
Changes don't apply until relogin. Under Wayland, KWin — not
plasma-kglobalaccel.service(which exits immediately) — owns theorg.kde.kglobalaccelD-Bus service, and it reads~/.config/kglobalshortcutsrconly at startup. After a rebuild the file is correct but the running KWin still holds the old binding, so the shortcut appears broken. Verify what is actually live, never trust the file:busctl --user call org.kde.kglobalaccel /kglobalaccel org.kde.KGlobalAccel \ shortcut as 4 kwin "<ActionName>" "" "" # returns Qt keycode intApply without relogin (
uflag 6 = SetPresent|NoAutoloading; KWin persists it back):busctl --user call org.kde.kglobalaccel /kglobalaccel org.kde.KGlobalAccel \ setShortcutKeys "asa(ai)u" 4 kwin "<ActionName>" "" "" 1 4 <keycode> 0 0 0 6Qt keycode =
0x10000000Meta |0x04000000Ctrl |0x08000000Alt |0x02000000Shift | keysym. -
Shift+<digit>must be written as the produced character. KWin removes Shift from the modifier mask when xkb marks it consumed producing the keysym, soMeta+Shift+1arrives asMeta+!and a literal"Meta+Shift+1"entry never matches.Meta+Shift+<letter>is unaffected. Layout is German (de, set inhosts/common.nixconsole.keyMap+modules/nixos/desktop.nixxkb.layout), so the shifted number row is! " § $ % & / ( ) =— NOT the US! @ # $ % ^ & * ( ).
Tool Replacements & Aliases
These are active shell aliases/overrides. Always use the right-hand side when suggesting commands to the user.
| Instead of | Use | Notes |
|---|---|---|
ls / ls -l |
eza / ll |
Icons, git status, group-dirs-first |
cat <file>.log |
tspin (auto via cat) |
auto-triggered for .log files and /var/log/* |
find |
fd |
Aliased: find → fd |
grep / rg |
rg |
Aliased: grep → rg |
du |
dust |
Aliased: du → dust |
ps |
procs |
Aliased: ps --tree |
top / htop |
btop |
Aliased: top → btop |
ping |
gping |
Aliased: ping → gping (graphical) |
curl |
xh |
Aliased: curl → xh |
cat (logs) |
tspin |
Shell function: auto for .log//var/log/* |
git diff |
difftastic |
Via git integration (programs.difftastic) |
git log |
gl |
Alias: git log --oneline --graph --decorate |
git status |
gs |
Alias |
git add |
ga |
Alias |
git commit |
gc |
Alias |
git push |
gp |
Alias |
lazygit |
lg |
Alias |
cd |
zoxide |
Auto-aliased with --cmd cd; learns frecency |
neofetch |
fastfetch |
|
nixos-rebuild switch |
rebuild / upnix |
Plays success/error sound; runs deploy.sh if present |
nix flake update |
update |
|
nh os list |
gen |
List NixOS generations |
nh clean all |
clean / gcnix |
Garbage collection wrapper |
history | grep |
hs "term" / Ctrl+R |
Alias: atuin search; Ctrl+R is atuin's TUI. Up/Down stay on zsh-history-substring-search |
tar/unzip/7z |
ouch |
Backs extract / pack; RAR still goes through unrar |
dig |
doggo |
Aliased: dig → doggo |
tmux |
zellij |
No tmux installed |
mkdir foo && cd foo |
mkcd foo |
Shell function |
sops |
secrets |
SOPS wrapper with configured keys |
termdown |
timer |
|
clock-rs -s |
stopwatch |
|
startplasma-wayland |
kde-plasma |
Start Plasma (Wayland) from TTY, like Hyprland |
claude |
cc-gemini |
Claude Code with Gemini backend |
claude (models) |
cc-models |
List available models on OpenRouter |
Available Shell Functions
upnix— rebuild NixOS (callsdeploy.sh switchif present)devnew <template>— init a dev shell fromdev-shells/templates (tab-completable)extract <archive>— universal archive extractor (ouch; unrar for.rar)pack <archive.ext> <files...>— inverse ofextractmkcd <dir>— mkdir + cd in onecat <file>.log— auto-usestspinfor log filescht <query>— interactive cheat sheet (cht.sh)dashboard— start and open Homepage (localhost:8082)paperless/paperless-stop— start/stop the on-demand Paperless-ngx stack (localhost:28981)ai-webui/ai-webui-stop— start/stop Open WebUI (whenfeatures.ai.openWebui.enable = true, localhost:8765)gcnix <keep>— clean nix garbage (default: keep 5 generations)rollback <N>— switch to generation N (gento list)cc-gemini,cc-kimi,cc-openrouter,cc-ollama— Claude Code backendsupdate-ai— bump thepkgs/ai/agent CLIs via nvfetcher (pkgs/ai/nvfetcher.toml→_sources/generated.json)
Any function that calls curl must write command curl: curl is aliased to
xh, and zsh expands aliases when the function body is parsed.
Local models must stay small — this host has an Intel iGPU and no dedicated
VRAM, so ~4B (gemma4:e4b) is the practical ceiling. OLLAMA_DEFAULT_MODEL
(functions.nix) and localModel (modules/home-manager/ai.nix) are the two
places to keep in sync. Frontier models are reached over an API, never locally:
Kimi K3 is 2.8T parameters (104B active) and ships no small variant, so
cc-kimi is an API wrapper by necessity, not by preference.
Security Hardening — Known Tradeoffs
When suggesting kernel sysctl hardening for modules/nixos/security.nix, be aware of these conflicts with this system's use case:
| Sysctl | Risk | Reason |
|---|---|---|
kernel.yama.ptrace_scope = 1 |
Breaks Proton/Steam games & debugger attach | Proton uses ptrace internally; attaching gdb/strace to running processes also breaks. Do NOT add this — gaming is enabled on this host. |
kernel.unprivileged_bpf_disabled = 1 |
Monitoring tools need sudo | Tools like bpftrace, some btop features, rustnet/netscanner require elevated privileges. Acceptable tradeoff but warn the user. |
net.core.bpf_jit_harden = 2 |
Pairs with BPF above | Low daily impact, fine to keep. Note it also blinds constants in the sched_ext scheduler's own BPF program (features.kernel.scx), so it is not free on the scheduling hot path; drop to 1 if scx latency ever needs the headroom. |
Default safe set (already in security.nix): kptr_restrict, dmesg_restrict, rp_filter, tcp_syncookies, redirect/accept blocks, bpf_jit_harden. Skip ptrace_scope and be cautious with unprivileged_bpf_disabled.
DO NOT
- Add packages to both system and user config
- Create verbose section comments
- Put assets in random locations
- Hardcode backup extension versions
- Add explanatory AI comments
- Touch
dev-shells/orarchiv/ - Create new files without following the structure
- Duplicate theme/icon definitions