- Nix 91.9%
- CSS 4.1%
- Lua 3.8%
- Shell 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .opencode/skills | ||
| modules | ||
| secrets | ||
| .gitignore | ||
| .sops.yaml | ||
| AGENTS.md | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
dotfiles
A flake-based NixOS + Home Manager configuration for user s8p, built on
flake-parts, import-tree, and a dendritic module registry.
Files in this repo represent features, not configuration layers. There is no
nixos/ vs home/ split — a feature such as Hyprland or Stylix owns its NixOS
side, its Home Manager side, and any runtime scripts or Lua it needs, all in one
place.
Table of contents
- Concepts
- Layout
- Hosts
- Feature registry
- Custom options
- Theming (Stylix)
- Hyprland
- Secrets (SOPS)
- Zen Browser
- Shell
- Working on this repo
- Adding things
- Conventions
Concepts
Three ideas carry the whole configuration.
1. flake.nix is thin. It declares inputs and hands ./modules to
import-tree. Nothing else:
outputs = inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules);
2. Every feature registers itself into a shared registry. A module doesn't get imported by a parent — it publishes itself under a name:
{ ... }:
{
flake.modules.nixos.pipewire = { ... }: {
security.rtkit.enable = true;
services.pipewire = { enable = true; /* ... */ };
};
}
Everything under modules/ is auto-discovered by import-tree and evaluated as
a flake-parts module. The registry then holds flake.modules.nixos.<feature> and
flake.modules.homeManager.<feature> attribute sets.
3. Hosts compose explicitly. A host picks the features it wants by name. There are no hidden transitive imports:
flake.nixosConfigurations.vmtest = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = with config.flake.modules.nixos; [
nix nixpkgs networkmanager locale
home-manager nh sops
containers git zsh s8p
pipewire bluetooth keyboard
hyprland greetd stylix extra-fonts
vmtest vmtest-secrets
];
};
A feature is free to expose only one module class. Not everything needs both a NixOS and a Home Manager branch.
Underscore-prefixed paths
import-tree treats every discovered .nix file as a top-level flake-parts
module. Files that are not flake-parts modules — ordinary NixOS/HM fragments,
helper functions, data tables — live under a _-prefixed directory so the
scanner skips them:
modules/shell/tmux/
├── default.nix # public: registers flake.modules.homeManager.tmux
├── _parts/ # private: plain HM fragments, imported by default.nix
├── _scripts/ # private: writeShellApplication helpers
├── _plugins/ # private: plugin set
└── layouts/ # data: tmuxifier session layouts
Same rule for _lua/, _extensions/, _boot.nix, _hardware.nix.
Layout
flake.nix # inputs + import-tree entry point
.sops.yaml # age recipients and per-file key groups
secrets/ # SOPS-encrypted YAML (shared.yaml, vmtest.yaml, …)
AGENTS.md # agent working rules for this repo
.opencode/skills/ # project-specific procedures (dendritic, sops, hyprland, …)
modules/
├── core/ # flake-parts wiring, nix settings, nixpkgs, locale, nh, HM integration
├── users/ # s8p — user account + the HM feature list they get
├── hosts/ # per-machine: host.nix, secrets.nix, _boot.nix, _hardware.nix
├── desktop/ # hyprland, waybar, stylix, greetd, mako, fuzzel, ghostty,
│ # wallpaper, screenshots, cliphist, nwg-displays, pcmanfm, fonts
├── shell/ # zsh, tmux, zoxide
├── cli/ # bat, cli-programs, cli-utilities
├── development/ # git, neovim, direnv, tools, secretspec
├── applications/ # zen-browser, obsidian, vesktop
├── virtualisation/ # containers (docker/podman), container-tools, container-nvidia, qemu-guest
├── networking/ # networkmanager, ssh
├── audio/ # pipewire
├── hardware/ # bluetooth
├── input/ # keyboard
└── security/ # sops
Hosts
| Host | Role | stateVersion |
Notes |
|---|---|---|---|
vmtest |
Test VM | 26.05 |
Full desktop. Preferred target for static build checks. |
devlab |
Physical dev machine | 25.05 |
Headless-ish; core + shell + git + user only. |
A host module is three things: local hardware/boot imports, host policy, and the
nixosConfigurations entry. vmtest is the fully worked example — it sets
keyboardConfig, containerRuntime, theme.colorScheme, and its own Home
Manager branch:
flake.modules.nixos.vmtest = { ... }: {
imports = [ ./_hardware.nix ./_boot.nix ];
networking.hostName = "vmtest";
system.stateVersion = "26.05";
keyboardConfig = {
consoleKeyMap = "br-abnt2";
xkb = { layout = "br"; variant = ""; options = [ "altwin:swap_alt_win" ]; };
};
containerRuntime = { enable = true; engine = "docker"; users = [ "s8p" ]; };
theme.colorScheme = "ayu-dark";
home-manager.users.s8p.imports = [ config.flake.modules.homeManager.vmtest ];
};
Host-specific user behavior goes in a matching
flake.modules.homeManager.<host> branch — for vmtest, that's the wallpaper
directory.
Boot config, _hardware.nix, system.stateVersion, and the secrets identity are
per-host and must be preserved when touching a host.
Feature registry
Names available for host composition.
NixOS (flake.modules.nixos.*)
| Group | Features |
|---|---|
| Core | nix, nixpkgs, locale, nh, home-manager |
| Security | sops, <host>-secrets |
| Desktop | hyprland, stylix, greetd, extra-fonts |
| System | pipewire, bluetooth, networkmanager, keyboard, zsh, git |
| Virt | containers, container-nvidia, qemu-guest |
| User/Host | s8p, vmtest, devlab |
Home Manager (flake.modules.homeManager.*)
| Group | Features |
|---|---|
| Base | s8p, stylix, keyboard |
| Shell | zsh, tmux, zoxide, bat, cli-programs, cli-utilities |
| Dev | git, neovim, direnv, development-tools, secretspec, ssh |
| Desktop | hyprland, waybar, fuzzel, mako, ghostty, pcmanfm, cliphist, nwg-displays, screenshots, wallpaper, hyprpolkitagent |
| Apps | zen-browser, obsidian, vesktop |
| Containers | container-tools |
| Host branches | vmtest |
The user module (modules/users/s8p.nix) is where the Home Manager feature list
is assembled — it defines the NixOS account and imports the HM features by name:
home-manager.users.s8p.imports = with config.flake.modules.homeManager; [
stylix s8p secretspec
cli-programs cli-utilities development-tools container-tools
bat git hyprland fuzzel mako pcmanfm ghostty nwg-displays
cliphist screenshots wallpaper waybar direnv tmux zsh zoxide
keyboard neovim obsidian vesktop zen-browser ssh hyprpolkitagent
];
Package bundles are kept deliberately small and named: cli-utilities is
curl file jq p7zip pv ripgrep unzip wget; development-tools is
lazygit nixfmt statix deadnix shellcheck shfmt yq-go just; container-tools
is lazydocker skopeo dive.
Custom options
Features define their own options rather than exposing raw upstream config. Hosts set these; the feature translates them.
theme.colorScheme
theme.colorScheme = "ayu-dark"; # base16-schemes name, no .yaml
Defined in both module classes. The NixOS side propagates the host's choice into
every Home Manager user via home-manager.sharedModules.
keyboardConfig
keyboardConfig = {
consoleKeyMap = "br-abnt2";
xkb = { layout = "br"; variant = ""; options = [ "altwin:swap_alt_win" ]; };
};
Drives console.keyMap and services.xserver.xkb, and propagates xkb into
Home Manager (where Hyprland consumes it) — but only when Home Manager is
actually present on that machine.
containerRuntime
containerRuntime = {
enable = true;
engine = "docker"; # or "podman"
users = [ "s8p" ]; # rootless/group access
dockerCompat = true; # podman only
autoPrune = { enable = true; dates = "weekly"; };
};
container-nvidia asserts this is enabled before wiring
hardware.nvidia-container-toolkit (pinned to device-name-strategy = "index",
so devices are nvidia.com/gpu=0, =1, =all).
desktop.wallpaper
desktop.wallpaper = {
enable = true;
directory = "/home/s8p/Images/Wallpapers";
prefixes = { horizontal = "h_"; vertical = "v_"; };
resize = "crop"; # crop | fit | stretch | no
};
Nix owns the mechanism, not the images. A wallpaperctl helper detects monitor
orientation at runtime and picks a matching image by filename prefix; the
collection stays mutable on disk and out of the Nix store. Runs awww as a
systemd user service.
desktop.screenshots
desktop.screenshots = {
enable = true;
directory = "${config.home.homeDirectory}/Images/screenshots";
};
Installs grim/slurp plus three writeShellApplication commands —
screenshotRegion, screenshotEdit, screenshotScreen — bound to the Print
key family in Hyprland.
greeter
greeter = {
sessionCommand = "start-hyprland";
numlock = true; # setleds +num before tuigreet, never fatal
};
Theming (Stylix)
Current policy:
- Base16 scheme: Tokyo Night Storm (default;
vmtestoverrides toayu-dark) - Icons: Colloid
- Cursor:
oreo_white_cursors, size 5 (intentionally small) - Fonts: JetBrainsMono Nerd Font + Noto Sans/Serif/Color Emoji
Both module classes share one mkTheme function, so NixOS and Home Manager can
never drift. Two settings matter and are load-bearing:
# NixOS side — we import the HM Stylix module ourselves
stylix.homeManagerIntegration.autoImport = false;
# HM side — Home Manager uses the NixOS pkgs instance (useGlobalPkgs)
stylix.overlays.enable = false;
Target-specific overrides belong with the feature, not with Stylix. Waybar, for example, disables Stylix's stock CSS in its own module because it ships custom CSS:
stylix.targets.waybar = { addCss = false; colors.enable = true; fonts.enable = true; };
Hyprland
Uses Hyprland's native Lua configuration (configType = "lua"), not legacy
Hyprlang.
modules/desktop/hyprland/
├── default.nix
└── _lua/
├── animations.lua
├── binds.lua
└── rules.lua
Nix generates a lib.vars Lua module carrying Nix-store-resolved executable
paths; the static Lua consumes it with require("lib.vars"). No hardcoded
/nix/store/... in the Lua files.
Long-running desktop programs register themselves through
desktop.autostart.programs. Features choose defaults with lib.mkDefault;
user or host modules can override enable and startAt. The central
autostart feature maps merged declarations to supervised systemd user units.
Structural rules:
- Ordinary config (
input,general,decoration,misc) goes under HMsettings.config→ becomeshl.config({...}). - Top-level generated settings like
monitorandbindmap to nativehl.*APIs. Do not inventsettings.input— it generates an invalidhl.input(...)call. - Long-running processes (Mako, hyprpolkitagent, awww, cliphist, Waybar) are systemd user services, not Hyprland startup hooks.
nwg-displaysowns~/.config/hypr/monitors.luaandworkspaces.lua. These are mutable and loaded defensively withpcall(require, ...)— never replaced by Home Manager symlinks.
Keybindings
SUPER is mainMod.
| Bind | Action |
|---|---|
mod + Return |
Terminal |
mod + D |
Launcher (fuzzel) |
mod + E |
File manager |
mod + Q |
Close window |
mod + H/J/K/L |
Focus left/down/up/right |
mod + Shift + H/J/K/L |
Swap window |
mod + Space |
Toggle float |
mod + F |
Toggle maximized |
mod + T |
Toggle split |
mod + P |
Toggle pseudo |
mod + Tab |
Cycle window, raise to top |
mod + S / mod + Shift + S |
Special workspace magic — toggle / move to |
mod + 1‥9 / mod + Shift + 1‥9 |
Focus / move to workspace |
mod + V |
Clipboard history (cliphist → fuzzel → wl-copy) |
mod + B |
Color picker (hyprpicker) |
mod + N |
Toggle wayscriber daemon |
mod + ' |
Keyboard layout picker |
Print / Shift + Print / mod + Print |
Screenshot region / edit / full screen |
mod + LMB / mod + RMB |
Drag / resize window |
Secrets (SOPS)
sops-nix, with a per-machine age identity at /var/lib/sops-nix/key.txt.
Keys are never generated by the configuration:
sops.age = { keyFile = "/var/lib/sops-nix/key.txt"; generateKey = false; };
.sops.yaml maps public recipients to files: secrets/<host>.yaml is readable
by s8p + that host; secrets/shared.yaml by s8p + all hosts.
A host's secrets module declares only its file and the exposed secret names:
flake.modules.nixos.vmtest-secrets = { ... }: {
sops = {
defaultSopsFile = ../../../secrets/vmtest.yaml;
secrets.test_secret.owner = "s8p";
secrets.litellm_api_key = {
sopsFile = ../../../secrets/shared.yaml;
owner = "s8p";
};
};
};
sops-key
A writeShellApplication helper shipped by the sops feature:
sops-key init # create this machine's age identity (mutates machine state)
sops-key public # print this machine's public age recipient
sops-key status # identity path + public recipient
Onboarding a new host: sops-key init → add the printed age1... to
.sops.yaml → sops updatekeys secrets/<host>.yaml.
Never commit: /var/lib/sops-nix/key.txt, personal age private keys,
plaintext secrets, or API tokens. Public age1... recipients in .sops.yaml are
safe.
Consumers should read runtime secret paths — never interpolate plaintext into a Nix derivation.
secretspecis for developer/application credentials and project-level API key declarations. It is not a substitute for boot/service secrets.
Zen Browser
The most involved application feature. default.nix wires
inputs.zen-browser.homeModules.beta and assembles everything from _parts/:
modules/applications/zen-browser/
├── default.nix
├── _parts/
│ ├── settings.nix # about:config prefs (compact mode, telemetry off, …)
│ ├── policies.nix # enterprise policies + Redirector rule injection
│ ├── extensions.nix # addon package list
│ ├── search.nix # search engines (default: Brave; + MyNixOS, …)
│ ├── containers.nix # Personal / Work / Shopping
│ └── redirects.nix # YouTube → Invidious rewrite rules
└── _extensions/reddit-invidious/ # locally-built MV3 extension
Extensions come from firefox-addons (LanguageTool, Bitwarden, uBlock Origin,
SponsorBlock, Dark Reader, Violentmonkey, Temporary Containers, Vimium C, React
DevTools, SingleFile, Karakeep, Stylus), plus two declarative ones:
redirector-declarative — whose rules are generated in redirects.nix and
injected through browser policies — and reddit-invidious, an MV3 extension
built in-tree that rewrites Reddit's YouTube embeds into Invidious links. The
Invidious instance URL is a single variable in default.nix.
Containers and search are declared with force/containersForce, so they are
authoritative over profile state.
Shell
Zsh — public module plus _parts/ for completion, prompt, keybindings,
exports. Syntax highlighting, autosuggestions, dedup'd appended history, and
history substring search on ^P/^N.
Tmux — prefix C-f, vi keys, mouse on, baseIndex = 1, tmux-256color,
custom pane navigation. Split with | and -. Helper commands live in
_scripts/ as writeShellApplication derivations (container status, service
status, popup, tmuxifier picker) so runtime deps are explicit. Session layouts
under layouts/.
Zoxide — its own feature (not buried in Zsh), aliased over cd via
--cmd cd. Standalone tools own their shell integration.
Aliases — cat → bat --paging=never. EDITOR/VISUAL/SUDO_EDITOR →
nvim, from a private flake input.
Working on this repo
Validate
nix flake show
nix flake check
nix flake show may warn that the generic modules output is unknown — expected,
since flake.modules is a custom registry.
Full host build without activating:
nix build --no-link .#nixosConfigurations.vmtest.config.system.build.toplevel
Targeted evaluation when debugging module wiring:
nix eval .#modules.nixos --apply builtins.attrNames
nix eval .#modules.homeManager --apply builtins.attrNames
nix eval .#nixosConfigurations.vmtest.config.<option>
Untracked files: git-backed flakes ignore untracked files. Either git add
(without committing) the files for the task at hand, or validate against a path
flake:
nix flake check "path:$PWD"
Apply
Applying a generation is a manual, human step:
nh os test # try it, no bootloader entry
nh os switch # commit it
programs.nh.flake defaults to /home/s8p/.config/dotfiles.
Note for automated agents: see
AGENTS.md. Agents may edit files and run non-activating evaluation/build checks, but must never activate — nonh os test/switch,nixos-rebuild test/switch,home-manager switch,switch-to-configuration,systemctl start|stop|restart|enable|disable, reboot, orsops-key init. Read-only inspection (systemctl status,journalctl,nix eval) is fine. Activation and runtime testing belong to the user.
Adding things
A new feature
Create one file named for the feature and register it:
# modules/desktop/swaylock.nix
{ ... }:
{
flake.modules.homeManager.swaylock = { pkgs, ... }: {
programs.swaylock.enable = true;
};
}
Then wire it: a Home Manager feature into modules/users/s8p.nix, a NixOS
feature into the relevant host's nixosConfigurations module list. Nothing is
active until a host or user asks for it.
If the feature grows implementation files, move to a directory with a public
default.nix and _parts/.
A new host
modules/hosts/<name>/
├── host.nix # flake.modules.nixos.<name> + flake.nixosConfigurations.<name>
├── secrets.nix # flake.modules.nixos.<name>-secrets
├── _boot.nix
└── _hardware.nix
- Generate its own
_hardware.nix; keep its existing boot config. - Preserve its existing
system.stateVersion. - Compose shared features explicitly in the module list.
- Give it its own age identity (
sops-key init) and secrets file, add the recipient to.sops.yaml, thensops updatekeys.
Conventions
- Organize by feature/concept, never by
nixos/vshome/. - A feature may expose one or several module classes — don't force both.
- Keep host hardware and boot local to the host.
- Prefer explicit host composition over hidden transitive imports.
- No growing catch-all
base.nix. Create focused features. - Avoid
lib.mkForceunless there's a demonstrated merge conflict that can't be solved more narrowly.lib.mkDefaultis used liberally so hosts can override. - No Home Manager-side
nixpkgs.overlaysornixpkgs.configwhileuseGlobalPkgs = true. - Use
pkgs.writeShellApplicationfor standalone helper commands so runtime dependencies are explicit. - Change the narrowest responsible layer. Separate structural moves from behavioral changes; get the move passing first.
- Don't introduce new frameworks or abstraction layers just to reduce repetition. Abstract after repeated real use shows the boundary.
Unfree packages are allowlisted individually, not globally:
nixpkgs.config.allowUnfreePredicate = pkg:
builtins.elem (lib.getName pkg) [ "obsidian" "languagetool" ];
Inputs
nixpkgs (unstable), flake-parts, import-tree, home-manager, sops-nix,
stylix, zen-browser, firefox-addons (rycee NUR),
firefox-extensions-declarative, and nvim — a private Neovim flake hosted on
git.tn.rsbp.dev. Everything follows the root nixpkgs.
Project-specific agent procedures live in .opencode/skills/: dendritic-nix,
static-nixos-validation, sops-host-secrets, hyprland-lua,
home-manager-feature.