Welcome

Hi! Welcome to the documentation site for my (Owais) dotfiles & flakes. More specifically, my NixOS workstation, dotfiles, editor setup, terminal workflow, and the parts that can be reused outside NixOS.

The repo is written for two audiences:

  • me, when I need to rebuild a machine or remember why something is configured a certain way
  • anyone trying to copy pieces of the setup on NixOS, Fedora, Ubuntu, Debian, or another Linux distro

Hyprland desktop preview Hyprland + Ghostty with fastfetch on nix-haxorus

Summary

What I Use

This is the working set behind the screenshot, not a full package inventory. The deeper pages document the behavior worth remembering.

AreaCurrent choice
SystemNixOS or Fedora, flakes, Home Manager, and host-specific modules.
DEGNOME, Hyprland, or XFCE (machine dependent)
TermGhostty, usually running Zellij.
ShellZsh with Starship.
EditorNeovim & Zed (vim-mode)
NotesNeovim & Obsidian
ReadingZathura with a compact dark interface.
NetworkingTailscale for stable hostnames and private services.

Entrypoints

Guidelines

If you are on NixOS, start with NixOS, then read Hosts and Secrets.

If the underlying Nix ideas are unclear, read Nix Concepts.

If you are not on NixOS, start with Other Distros. Then use the program pages for the tools you want to copy.

If you are editing this repo, read Development and Writing docs.

Structure

conf/
├── machines/      # host-specific NixOS config
├── modules/       # app config and focused modules
├── secrets/       # SOPS-encrypted secrets
└── shared.nix     # shared NixOS and Home Manager modules

docs/src/          # This book's source (mdBook)
shells/            # project shell helpers

Guides

Common commands

# Build, test, and switch
sudo nixos-rebuild build --flake .#$(hostname)
sudo nixos-rebuild test --flake .#$(hostname)
sudo nixos-rebuild switch --flake .#$(hostname)

# Inspect generations
nixos-rebuild list-generations
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system

# Garbage collect
sudo nix-collect-garbage -d
nix store optimise

# Update inputs
nix flake update
nix flake lock --update-input nixpkgs

# Hardware config
sudo nixos-generate-config --show-hardware-config \
  > conf/machines/$(hostname)/hardware-configuration.nix

conf/shared.nix defines zsh aliases for common rebuild commands.

Home Manager

Home Manager is wired through the NixOS flake, so normal nixos-rebuild applies both system and home changes. The shared Home Manager module is (import ./conf/shared.nix).home in flake.nix.

Secrets

SOPS_AGE_KEY_FILE=$(pwd)/age.txt nix shell nixpkgs#sops -c sops conf/secrets/owais.yaml
SOPS_AGE_KEY_FILE=$(pwd)/age.txt nix shell nixpkgs#sops -c sops -d conf/secrets/owais.yaml
./conf/scripts/keys.sh

./conf/scripts/keys.sh extracts Git provider SSH keys into ~/.local/share/sops/ for non-NixOS use.

Check changes

When making small Nix changes (like adding packages), a quick local check flow:

# 1) Format (keeps diffs small and avoids nixfmt-related CI/lint churn)
nixfmt conf/shared.nix

# 2) For a changed Nix file, check syntax without evaluating the module
nix-instantiate --parse conf/shared.nix

# 3) Ensure the flake evaluates and outputs are well-formed
nix flake show --no-write-lock-file

# 4) Optional, stronger checks before switching
sudo nixos-rebuild build --flake .#$(hostname)
sudo nixos-rebuild test --flake .#$(hostname)

Notes:

  • nix-instantiate normally evaluates Nix expressions and produces store derivation paths from expressions that evaluate to derivations. The manual describes it as instantiating store derivations from Nix expressions.
  • nix-instantiate --parse FILE is much narrower: it only parses the file and prints the abstract syntax tree as a Nix expression. It is useful as a quick syntax check after editing a module.
  • --parse does not evaluate imports, module options, flake outputs, package names, paths, assertions, or type checks. A file can parse successfully and still fail during nix flake show or nixos-rebuild.
  • nix flake show only evaluates the flake; it does not build anything.
  • nixos-rebuild build/test will actually evaluate/build the system closure and will catch missing packages/options earlier than switch.

Troubleshooting

  • Use sudo nixos-rebuild test --flake .#host before switching.
  • If Home Manager reports file collisions, move the existing file and rebuild.
  • If secrets fail, confirm /var/lib/sops-nix/key.txt exists on NixOS or set SOPS_AGE_KEY_FILE manually for local operations.
  • If a flake path changed, search with rg 'old/path' and update docs, scripts, and Nix imports together.

Migration checklist

  • Copy hardware config into conf/machines/{machine}/.
  • Keep host-only options in that machine's configuration.nix.
  • Keep reusable system and home settings in conf/shared.nix.
  • Keep host-specific desktop stacks in opt-in modules under conf/modules/de/.
  • Rebuild with sudo nixos-rebuild switch --flake .#{hostname}.

Writing docs

I'm a bit pedantic when it comes to my writing.

The key (for me) is to write these docs like working notes and not marketing copy.

Use direct sentences.

Name the command, file, or setting.

Keep filler to a minimum and focus on readability. Cut filler such as

Prefer:

Run `nix flake show --no-write-lock-file` after editing `flake.nix`.

Avoid:

It's worth noting that this command serves as a useful way to validate the flake.

Checklist

Quality

  • Ask yourself if you'll actually read this. Overly long/verbose documentation is ostensibly useless.
  • Do not summarize a section after already explaining it.
  • Keep one point per paragraph.

Style

  • Use commands and paths when they answer the question faster than prose.
  • Avoid vague claims such as "important", "powerful", or "useful" unless the sentence explains why.
  • Remove filler openings.
    • "it's worth noting"
    • "the goal is"
    • "this serves as"
    • "despite these challenges".
  • Replace passive voice when the actor matters.

Nix Concepts

This chapter is for teaching Nix and explaining the moving parts behind this repository. Operational pages should link here instead of re-explaining flakes, modules, or Home Manager each time they mention them.

TopicUse it for
Language basicsReading Nix expressions and small snippets.
FlakesUnderstanding inputs, outputs, and the lock file.
ModulesUnderstanding how NixOS and Home Manager compose settings.
NixOS and Home ManagerHow system and user config fit together in this repo.

The rest of the book should stay practical: what owns a service, how to rebuild it, what to check when it breaks, and which decisions matter.

Language Basics

Nix is an expression language. Files evaluate to values: strings, paths, lists, attribute sets, functions, or larger structures built from those pieces.

FormMeaning
"hello"String
./file.nixPath, copied into the Nix store when referenced
[ a b c ]List
{ key = value; }Attribute set
x: x + 1Function
{ pkgs, ... }: { }Function taking an attribute set
let name = value; in bodyLocal binding

Attribute sets are the central shape. NixOS modules, Home Manager modules, flake inputs, flake outputs, package sets, and option trees are all attribute sets at different layers.

Impurities

Most Nix expressions are pure: they transform values into other values without observing the outside world. The exception to know is how Nix introduces files as build inputs.

The common case is a path such as ./data or ./src. A path literal is not just a string. When Nix needs it as a build input, Nix hashes the referenced file or directory, copies it into /nix/store, and uses the resulting store path. Directories are copied as whole directory trees.

Fetchers apply the same pattern to remote inputs. Functions such as builtins.fetchurl, builtins.fetchTarball, and builtins.fetchGit fetch source material and return a store path. If the fetch cannot complete, the expression cannot be evaluated.

Other impure expressions exist, including lookup paths such as <nixpkgs> and host-dependent values such as builtins.currentSystem. When reading older examples, treat these as values that depend on evaluator state outside the expression itself.

Derivations

A derivation is a build description. The Nix language describes derivations, Nix realizes them, and the resulting store paths can be used as inputs to later derivations.

The low-level primitive is derivation, but most Nix code uses wrappers such as stdenv.mkDerivation, mkShell, language-specific builders, or NixOS system builders. Those wrappers still produce derivations underneath.

Evaluating a derivation does not necessarily build it immediately. Evaluation produces the build description and expected output path. Realization is the later step where Nix builds the output locally or substitutes it from a binary cache.

Derivations also work in string interpolation. If pkg is a derivation, "${pkg}" evaluates to the store path of its build result. That lets one derivation refer to another derivation's output as an input while the Nix language still treats packages as composable values.

Common Idioms

Imports compose modules:

{
  imports = [ ./hardware-configuration.nix ];
}

Package lists are ordinary lists:

home.packages = with pkgs; [
  fd
  zathura
];

with pkgs; brings package names into scope for the expression that follows. It is convenient for package lists, but avoid using it when it would make the origin of names unclear.

Read structured files with builtins when a native parser is available:

builtins.fromJSON (builtins.readFile ./config.json)

Prefer data formats and module options over ad hoc string generation. Generated text is useful for application config files, but it should not be the first tool for structured data.

References

Flakes

A flake is a pinned project interface. It declares inputs, records exact input revisions in flake.lock, and exposes outputs that commands can build or inspect.

PartRole
inputs.nixpkgsMain Nixpkgs channel for systems and packages.
inputs.nixpkgs-unstableFast-moving packages, currently used for Zed & Claude Code.
inputs.home-managerUser environment as part of each NixOS system.
inputs.sops-nixSecret material decrypted onto NixOS hosts.
outputs.nixosConfigurationsBuildable NixOS hosts.

The important command target is:

.#nixosConfigurations.<hostname>

nixos-rebuild --flake .#<hostname> is shorthand for selecting one of those host outputs.

Lock File

flake.lock is part of the system definition. Rebuilding without changing it uses the same upstream source graph. Updating it changes package and module inputs, so treat lock updates as intentional system changes.

Use targeted updates when possible:

nix flake lock --update-input nixpkgs-unstable

Use broad updates when the task is a full system refresh:

nix flake update

After changing inputs, evaluate or test one host before switching every host.

Modules

NixOS and Home Manager are module systems. A module is a Nix expression that returns option assignments. Imported modules are merged into one final option tree.

This is why different files can contribute to the same service, package list, user account, or config file without manually concatenating everything.

Merge Model

ConceptMeaning
importsAdds more modules to the current module.
OptionsDeclared settings such as services.openssh.enable.
ValuesAssignments to options.
MergeThe module system combines compatible values and reports conflicts.
lib.mkIfAdds settings conditionally.
specialArgsExtra values passed to modules at evaluation time.
  • Lists usually concatenate.
  • Attribute sets usually merge by key.
  • Some options define custom merge behavior.
  • Conflicting scalar values fail evaluation unless priority helpers are used.

Repository Shape

conf/shared.nix exports two reusable modules:

  • (import ./conf/shared.nix).nixos
  • (import ./conf/shared.nix).home

Machine modules import the shared NixOS module, then add host-specific options. Home Manager is attached from flake.nix so user configuration is built with the system.

In general, keep generalized behavior in shared modules but keep hardware, one-off services, and host-only desktop stacks in host modules or opt-in modules.

NixOS And Home Manager

NixOS owns system state. Home Manager owns user state. This repo wires Home Manager into each NixOS host, so one rebuild applies both.

LayerOwns
NixOSboot, users, system services, hardware, system packages, secrets
Home Managerdotfiles, user packages, editor config, shell config, desktop user config
Host modulesmachine-specific hardware and opt-in services
Shared modulesbaseline system and user behavior for every host

This split keeps the source of truth clear:

  • If it needs root, systemd system units, hardware, or /run/secrets, it is usually NixOS.
  • If it writes files under /home/owais, starts user services, or configures applications, it is usually Home Manager.
  • If only one machine should have it, keep it out of conf/shared.nix.

Rebuild Flow

test activates a generation without making it the boot default:

sudo nixos-rebuild test --flake .#$(hostname)

switch activates it and makes it the boot default:

sudo nixos-rebuild switch --flake .#$(hostname)

Use test first for changes that can affect boot, networking, login shells, display managers, secrets, or Home Manager activation.

Development

This repository is a NixOS flake. Most changes are edits to conf/shared.nix, a machine file under conf/machines/, or documentation under docs/src/.

Common checks:

nixfmt conf/shared.nix
nix flake show --no-write-lock-file
sudo nixos-rebuild build --flake .#$(hostname)

Use test before switching when a change could affect services, desktop startup, login shells, or Home Manager activation:

sudo nixos-rebuild test --flake .#$(hostname)

The zsh aliases in conf/shared.nix wrap the common rebuild commands:

rebuild  # switch
switch   # switch
nboot    # boot
tbuild   # test

Docs are an mdBook in docs/. Edit source files in docs/src/; docs/book/ is generated output.

cd docs
mdbook serve

Dev environments

Some application projects need native Linux libraries that should not live in this machine's global profile. Tauri is the main example: Rust crates such as glib-sys, gtk-sys, gdk-sys, and webkit2gtk-sys discover system libraries through pkg-config. On NixOS those .pc files are not globally visible unless a shell or package build exposes them.

For those cases, keep the dependency set in the application project as a shell.nix. This keeps the global system configuration small and makes the app build environment explicit.

Tauri

dev/tauri.nix contains a dev shell for the Tauri apps.

The shell provides:

  • Rust tooling: cargo, rustc, clippy, rustfmt, rust-analyzer
  • frontend tooling: nodejs_24, pnpm
  • build discovery: pkg-config
  • Tauri Linux libraries: GTK, GLib, WebKitGTK, libsoup, appindicator, and friends
  • SQLite headers and library for libsqlite3-sys

If a Tauri build reports a missing package such as glib-2.0, gdk-3.0, or sqlite3, add the Nix package to the app's shell.nix. Do not add these Tauri libraries to conf/shared.nix unless they are actually useful system-wide.

Neovim

Neovim is managed by Home Manager in conf/shared.nix, while the actual editor configuration comes from the external neovim-config flake input.

home.file.".config/nvim" = {
  source = neovim-config;
  recursive = true;
};

Defaults

  • programs.neovim.enable = true
  • viAlias, vimAlias, and defaultEditor are enabled.
  • Python and Ruby providers are kept enabled for compatibility.

Workflow

  • Edit the upstream Neovim config repo for plugin/keymap/theme changes.
  • Run nix flake update --update-input neovim-config here to pull changes.
  • Rebuild this system to install the updated config.

Quick keys

Common habits preserved from the existing config:

  • <leader> opens the main custom mappings namespace.
  • Telescope-style pickers are used for files, text, buffers, and help.
  • LSP mappings cover definition, references, rename, code actions, and hover.
  • Formatting and diagnostics are available through normal LSP commands.

Use Neovim's built-in helpers when unsure:

:map
:Telescope keymaps
:checkhealth

Debugging System Services

This is a quick checklist for debugging services.

Systemd Units

List failed system services:

systemctl --failed

List failed user services:

systemctl --user --failed

Inspect one unit and its recent logs:

systemctl status waybar.service
systemctl --user status hypridle.service

Use status for a human-readable snapshot. It includes runtime state and recent journal lines, while show is better for scripts.1

Journal Logs

journalctl reads systemd's structured journal and supports filters such as the current boot, system or user journal, unit names, and grep-style message matches.2

Read the current boot only:

journalctl -b --no-pager
journalctl --user -b --no-pager

Filter to a unit:

journalctl -b -u display-manager.service --no-pager
journalctl --user -b --user-unit waybar.service --no-pager

Search messages for a word or regex:

journalctl -b -g 'hyprlock|loginctl|pam' --no-pager

Crashes

coredumpctl debug opens the matching core in a debugger, and info PID prints metadata plus stack trace details when they are available.3

List and inspect coredumps:

coredumpctl list
coredumpctl info PID
coredumpctl debug PID

NixOS

This repo builds two NixOS hosts from one flake and a shared baseline.

HostMachine directoryNotes
nix-haxorusconf/machines/thinkpad/ThinkPad workstation with Hyprland.
nix-baxcaliburconf/machines/hp/HP host for shared services.

Repository Layout

PathPurpose
flake.nixHost outputs, inputs, and Home Manager wiring.
flake.lockExact upstream revisions.
conf/shared.nixShared NixOS and Home Manager modules.
conf/machines/*/configuration.nixHost-specific system choices.
conf/machines/*/hardware-configuration.nixGenerated hardware facts.
conf/modules/App config assets and focused local modules.
conf/services/Reusable service modules.
conf/secrets/owais.yamlSOPS-encrypted secrets.

For the concepts behind flakes and modules, see Nix Concepts.

Rebuilds

Use the hostname as the flake target:

sudo nixos-rebuild test --flake .#$(hostname)
sudo nixos-rebuild switch --flake .#$(hostname)

Prefer test before switch when touching boot, display managers, shells, networking, secrets, or Home Manager activation.

Inputs

Keep flake.lock stable unless the task is intentionally updating packages or modules. For targeted updates:

nix flake lock --update-input nixpkgs-unstable

For a broad refresh:

nix flake update

Evaluate at least one host after lock changes.

Secrets

SOPS-Nix decrypts configured secrets to /run/secrets/ on NixOS hosts. See Secrets for the active secret names and local edit commands.

Services

Shared service policy and host-specific service pages live under Services.

Adding Hosts

Use Adding a New Machine for the operational checklist.

Hosts

The flake builds two NixOS hosts. Both import conf/shared.nix and then add machine-specific options from conf/machines/{machine}/configuration.nix.

nix-haxorus

ThinkPad configuration.

Files:

  • conf/machines/thinkpad/configuration.nix
  • conf/machines/thinkpad/hardware-configuration.nix
  • conf/modules/de/hypr.nix
  • conf/modules/de/hypr-home.nix

Flake target:

sudo nixos-rebuild test --flake .#nix-haxorus
sudo nixos-rebuild switch --flake .#nix-haxorus

Host-specific settings:

  • hostname: nix-haxorus
  • thermal daemon enabled
  • TLP enabled
  • power-profiles-daemon disabled
  • default TLP mode set to battery
  • CPU governor set to performance on AC and powersave on battery
  • fingerprint daemon enabled
  • Hyprland desktop enabled through host-specific modules

nix-baxcalibur

HP configuration.

Files:

  • conf/machines/hp/configuration.nix
  • conf/machines/hp/hardware-configuration.nix

Flake target:

sudo nixos-rebuild test --flake .#nix-baxcalibur
sudo nixos-rebuild switch --flake .#nix-baxcalibur

Host-specific settings:

  • hostname: nix-baxcalibur

The HP file imports shared config and hardware config. GNOME is enabled through shared config; Hyprland stays ThinkPad-specific.

Add another host

See Adding a New Machine for the full workflow.

Secrets

Secrets are stored in conf/secrets/owais.yaml and encrypted with SOPS. NixOS hosts decrypt them with SOPS-Nix; non-NixOS machines can extract selected files with the helper script.

Active Secrets

SecretNixOS pathUsed by
keys_gh/run/secrets/keys_ghGitHub SSH
keys_codeberg/run/secrets/keys_codebergCodeberg SSH
keys_tangled/run/secrets/keys_tangledTangled and Knot SSH
umans_key/run/secrets/umans_keyClaude Code Umans API

Files are owned by owais:users with mode 0600.

NixOS Model

PieceRole
conf/secrets/owais.yamlEncrypted secret values.
.sops.yamlRecipient and file encryption policy.
/var/lib/sops-nix/key.txtAge key used by NixOS during activation.
conf/shared.nixDeclares which secrets should appear under /run/secrets.

SSH and Claude Code reference /run/secrets paths directly.

Local Editing

Use the local age key when editing from the repo:

SOPS_AGE_KEY_FILE=$(pwd)/age.txt nix shell nixpkgs#sops -c sops conf/secrets/owais.yaml

Validate decryptability without printing values:

SOPS_AGE_KEY_FILE=$(pwd)/age.txt nix shell nixpkgs#sops -c sops -d conf/secrets/owais.yaml >/dev/null

Update recipients after changing .sops.yaml:

nix shell nixpkgs#sops -c sops updatekeys conf/secrets/owais.yaml

Non-NixOS Extraction

conf/scripts/keys.sh extracts Git SSH keys for machines that do not use SOPS-Nix.

Expected inputOutput
~/.config/sops/age/keys.txtLocal age key
conf/secrets/owais.yamlEncrypted source
~/.local/share/sops/keys_ghGitHub key
~/.local/share/sops/keys_codebergCodeberg key
~/.local/share/sops/keys_tangledTangled key

Run:

mkdir -p ~/.config/sops/age
cp age.txt ~/.config/sops/age/keys.txt
./conf/scripts/keys.sh

The script sets key file permissions to 0600.

Common Failure Modes

SymptomLikely cause
SOPS cannot decrypt locallySOPS_AGE_KEY_FILE points at the wrong key.
NixOS rebuild cannot decrypt/var/lib/sops-nix/key.txt is missing or wrong.
SSH ignores a keyFile permissions or IdentityFile path are wrong.
Claude Code cannot authenticate/run/secrets/umans_key is missing after rebuild.

Adding A New Machine

This page is the operational checklist for adding a NixOS host to the flake.

For why flakes, modules, and Home Manager work this way, see Nix Concepts.

Naming

Pick two names before creating files:

NameExampleUsed for
Directoryframework, x1Human-readable folder under conf/machines/.
Hostnamehaxorusnetworking.hostName and flake target.

They may match, but they do not have to.

Existing hosts use short hardware directory names and Pokemon hostnames.

Checklist

StepFile or commandNotes
Generate hardware confignixos-generate-config --show-hardware-configStore it in the new host directory.
Add host moduleconf/machines/{machine}/configuration.nixImport hardware and shared NixOS config.
Set hostnamenetworking.hostName = "{hostname}";Must match the flake target you plan to build.
Add flake outputnixosConfigurations.{hostname}Copy the shape of existing hosts.
Choose architecturex86_64-linux or aarch64-linuxMatch the target CPU.
Build temporarilysudo nixos-rebuild test --flake .#{hostname}Safer first activation.
Make persistentsudo nixos-rebuild switch --flake .#{hostname}Only after the test generation works.

Host Directory

Create:

conf/machines/{machine}/
├── configuration.nix
└── hardware-configuration.nix

hardware-configuration.nix should stay mostly generated. It captures machine facts: filesystems, swap, kernel modules, CPU hints, and hardware-specific imports.

configuration.nix should contain host choices: hostname, hardware workarounds, desktop stack imports, power management, and services that belong only on that machine.

Keep general packages, users, shell defaults, editor defaults, and shared secrets in conf/shared.nix.

Minimal Host Module

{ ... }:

{
  imports = [
    ./hardware-configuration.nix
    (import ../../shared.nix).nixos
  ];

  networking.hostName = "{hostname}";
}

For a host-specific desktop stack, import its system module here. nix-haxorus does this for Hyprland.

Flake Output

Add a nixosConfigurations.{hostname} entry in flake.nix. Copy an existing host entry and adjust:

  • output name
  • system
  • machine configuration path
  • any host-specific Home Manager imports

This repo wires Home Manager through each NixOS system, so normal nixos-rebuild applies both system and user changes.

Activation

Build and activate temporarily:

sudo nixos-rebuild test --flake .#{hostname}

If it works, make it the boot default:

sudo nixos-rebuild switch --flake .#{hostname}

If a switched generation is bad:

sudo nixos-rebuild switch --rollback

Final Review

  • Hardware config is present and machine-specific.
  • Host module imports shared NixOS config.
  • Hostname and flake output name match.
  • Architecture matches the machine.
  • Host-only features stayed out of conf/shared.nix.
  • Shared behavior went into conf/shared.nix.
  • test succeeded before switch.

Services

Self-hosted service documentation lives here. Service pages should describe the operating model: ownership, boundaries, access paths, checks, and failure modes.

Implementation details stay in conf/services/ and host imports stay under conf/machines/.

Shared Baseline

Service areaBaseline
TailscaleEnabled on every NixOS host with firewall support.
OpenSSHEnabled on every NixOS host.
PostgreSQLLocal development database baseline.
RedisLocal development cache baseline.
DockerLocal container runtime baseline.

Service Pages

PageScope
TailscalePrivate network, MagicDNS, Serve, and tailnet policy.
Git ForgeForgejo on Baxcalibur.
KavitaComics, manga, ebooks, and PDFs on Baxcalibur.
Tangled KnotTangled knot and SSH guard on Baxcalibur.

Tailscale

Tailscale is the private network layer. It gives trusted devices stable tailnet identity, MagicDNS names, and encrypted paths to private services.

Ownership

LayerOwns
conf/shared.nixEnables tailscaled, opens firewall, installs CLI.
Tailscale admin consoleMagicDNS, HTTPS, device approval, expiry, users, groups, ACLs.
Service pagesHow individual services use tailnet access.

How This Repo Uses It

UsePolicy
HostnamesPrefer MagicDNS names such as nix-baxcalibur.
Git writesUse SSH over the tailnet.
Forgejo private/admin accessKeep on the tailnet.
KavitaPublish private HTTPS through Tailscale Serve.
Public exposureUse Funnel only when intentionally needed.

Host Setup

After a rebuild on a new host, authenticate once with sudo tailscale up. Approve the device in the admin console if the tailnet requires it.

For long-lived servers, disable key expiry only after confirming the device identity. Do not disable expiry for casual clients by default.

Checks

CheckCommand
Daemonsystemctl status tailscaled
Tailnet statetailscale status
Local tailnet IPtailscale ip
MagicDNStailscale ip nix-baxcalibur
SSH pathssh owais@nix-baxcalibur
Serve routestailscale serve status

Serve

Use Serve for private HTTPS access to a local service. The usual shape is a tailnet HTTPS endpoint forwarding to a service bound on 127.0.0.1.

ServiceLocal endpointExposure
Kavita127.0.0.1:5000Tailscale Serve
Forgejo admin/private use127.0.0.1:3030Tailnet first
Tangled Knot SSHsystem SSH on 22Tailnet SSH path

Funnel is public internet exposure. Treat it as temporary or explicitly intentional service policy, not the default.

Troubleshooting

SymptomCheck
Name does not resolveMagicDNS enabled, client on correct tailnet, device online.
SSH hangsACLs, MagicDNS result, and target sshd.
Serve URL missingTailnet HTTPS enabled and Serve route configured.
Device repeatedly expiresAdmin console expiry policy for that node.

References

Git Forge

Forgejo runs on Baxcalibur at https://git.desertthunder.dev.

Public repositories may be readable over HTTPS. Writes, admin access, private repositories, SSH remotes, and private LFS paths stay on the tailnet.

Tracked implementation tasks stay in TODO.md. This page is the durable service shape and operating model.

Current Shape

AreaValue
Moduleconf/services/forgejo.nix
Hostnix-baxcalibur
Local URL127.0.0.1:3030
Public URLhttps://git.desertthunder.dev/
DatabaseLocal PostgreSQL
State/var/lib/forgejo
Public ingressCloudflare Tunnel
Private control planeTailscale
RegistrationDisabled

Boundaries

PathPolicy
Public HTTPS readsCloudflare Tunnel to local Forgejo.
WritesSSH over the tailnet.
Admin/private repositoriesTailnet first.
LFS writesTailnet path to avoid Cloudflare free-tier limits.
SSH portShared system port 22, separated by SSH user.

The Cloudflare tunnel should only route the intended hostname and return 404 for unmatched hostnames.

Client Model

ClientExpected path
DesktopNormal Git over SSH to Baxcalibur’s tailnet name.
Android/TermuxGit and OpenSSH over Tailscale.
Obsidian vaultRepository-local sync helper once the vault repo exists.

Use per-device SSH keys. They are easier to revoke than copied private keys when a device or Termux install is retired.

SSH Remotes

Forgejo advertises SSH clone URLs with:

FieldValue
Hostnix-baxcalibur
Port22
Userforgejo

Repository remotes should look like:

git remote set-url origin forgejo@nix-baxcalibur:USER/REPO.git

If using a client-side SSH alias, keep the alias device-local and point it at the same tailnet hostname.

Checks

CheckCommand
Servicesystemctl status forgejo
Recent logsjournalctl -u forgejo -e
Tailnet SSHssh -T nix-baxcalibur-forgejo
Remote shapegit remote -v

NixOS restarts forgejo.service when generated service configuration changes. If a rebuild succeeds but the UI still shows old customization, restart the service explicitly.

References

Tangled Knot

The Tangled knot runs on Baxcalibur at https://knot.desertthunder.dev.

This service hosts the repositories and SSH guard for the ATproto identity that owns the knot.

Current State

The owner is configured as did:plc:xg2vq45muivyy3xwatcehspu, my ATproto handle desertthunder.dev.

If the handle changes to a different DID, update desert.services.tangledKnot.ownerDid.

Boundaries

Cloudflare Tunnel fronts public HTTPS for knot.desertthunder.dev and maps it to http://127.0.0.1:5555.

SSH uses the normal system sshd on port 22.

The upstream Tangled module adds a Match User git block that uses knot keys as AuthorizedKeysCommand, so Tangled-managed SSH keys authorize Git access for the git user.

Client machines use the nix-baxcalibur-knot SSH alias for Tailscale-friendly Git remotes.

That alias connects to the MagicDNS hostname nix-baxcalibur as git and uses the sops-managed Tangled key.

Forgejo also uses SSH on Baxcalibur, but it uses the forgejo user. The two services share port 22 by separating users:

  • Forgejo remotes: forgejo@nix-baxcalibur:USER/REPO.git
  • Tangled knot remotes: nix-baxcalibur-knot:USER/REPO.git

Use Tangled's repository path when setting the remote. For example:

git remote set-url origin nix-baxcalibur-knot:USER/REPO.git

Checks

systemctl status knot
journalctl -u knot -e
ssh -T git@nix-baxcalibur

Check the public HTTPS endpoint that Tangled sees:

curl 'https://knot.desertthunder.dev/xrpc/sh.tangled.knot.version'

If Tangled shows "The knot hosting this repository is unreachable" but the knot version endpoint and repo endpoints work, Tangled's mirror may need to re-crawl the repo. Ask the mirror to subscribe to the knot and ensure the repo record:

curl -X POST 'https://mirror.tangled.network/xrpc/sh.tangled.sync.requestCrawl' \
  -H 'content-type: application/json' \
  --data '{
    "hostname": "knot.desertthunder.dev",
    "ensureRepo": "at://did:plc:xg2vq45muivyy3xwatcehspu/sh.tangled.repo/garden"
  }'

Then re-check the mirror endpoint Tangled's overview page uses for README rendering:

curl 'https://mirror.tangled.network/xrpc/sh.tangled.git.temp.getBlob?repo=did:plc:r5jqwa23vgiar6cvmoeuqkvl&ref=main&path=README.md'

For other repositories, replace the ensureRepo AT URI with that repo's sh.tangled.repo record URI and replace the repo query parameter with its repo DID.

References

Kavita

Kavita runs on Baxcalibur for comics, manga, ebooks, and PDFs.

Access is tailnet-first. Use Tailscale Serve for private HTTPS access and Tailscale Funnel only when the library intentionally needs temporary public access.

Shape

Kavita state and media should be treated separately. State contains app config, library metadata, covers, settings, logs, and the SQLite database. Media is the book/comic/manga library itself and should have its own backup and storage policy.

The local module sets conservative service limits:

  • MemoryMax=1G
  • CPUQuota=150%
  • IOSchedulingClass=best-effort
  • IOSchedulingPriority=6

These limits are mainly for first scans, cover generation, and imports. Idle load should be modest.

Rebuild

After switching:

systemctl status kavita
journalctl -u kavita -f
curl http://127.0.0.1:5000/site.webmanifest

Media Layout

Do not point Kavita at ~/Documents or ~/Downloads. Keep a staging inbox and only move organized files into Kavita's library roots:

/srv/media/inbox
/srv/media/books
/srv/media/comics
/srv/media/manga

Kavita should scan only the real library roots, not the inbox.

Recommended layout:

/srv/media/books/Author or Collection/Book Title.epub
/srv/media/books/Author or Collection/Book Title.pdf
/srv/media/comics/Series Name/Series Name v01 - Volume Title.pdf
/srv/media/comics/Series Name/Series Name 001.cbz
/srv/media/manga/Series Name/Series Name v01.cbz

For comics and manga, use series folders. For books, author or collection folders are not strictly required, but they keep the library easier to maintain.

Move it into the library roots with:

sudo mkdir -p /srv/media/books /srv/media/comics
sudo cp -a ~/Downloads/inbox/kavita-ready/books/. /srv/media/books/
sudo cp -a ~/Downloads/inbox/kavita-ready/comics/. /srv/media/comics/
sudo chmod -R a+rX /srv/media/books /srv/media/comics

After verifying Kavita can scan the /srv/media copies, remove the staged copy:

rm -r ~/Downloads/inbox/kavita-ready

Client model

Primary readers should use the tailnet URL from desktop, Android, and tablet clients. OPDS or third-party clients can be added later if they are useful, but the web UI is the baseline supported client.

The initial admin user is created from the web UI after the service is first available. Library scan schedules should avoid maintenance windows and large media-copy operations.

Android access path:

  1. Install Tailscale.
  2. Sign in to the same tailnet.
  3. Open the Kavita HTTPS URL from tailscale serve status.
  4. Use the browser's "Add to Home screen" flow if an app-like shortcut is useful.

Reliability

Backups must cover /var/lib/kavita, especially the SQLite database, covers, settings, and logs. Media backups are separate and should be validated independently.

Health checks should use the tailnet endpoint. Public Funnel checks only matter while Funnel is intentionally enabled.

Disk monitoring should account for media, covers, cache, and database growth. Avoid automatic reboots during imports, scans, or metadata refreshes.

References

Other Linux Distributions

This repo is a NixOS workstation, but many user-level choices translate to Fedora, Ubuntu, Debian, and other Linux distributions. Treat the Nix files as an inventory and policy reference, not as something another distro can apply directly.

Target Shape

AreaExpected result
DesktopGNOME or Hyprland on Wayland, PipeWire, NetworkManager, printing, SSH.
ShellZsh login shell with Starship and a small alias set.
EditorsNeovim and Zed with language servers available on PATH.
TerminalGhostty, usually launching Zsh and sometimes Zellij.
Dev servicesDocker, PostgreSQL, and Redis for local development.
CLI toolsripgrep, fd, jq/yq, fzf, bat, direnv, just, and common compilers.
FontsNerd fonts plus Inter, Open Sans, Noto, and other UI/code fonts.
SecretsSOPS-extracted SSH keys under a normal user-owned path.

Translation Map

NixOS sourcePortable equivalent
environment.systemPackagesDistro packages, language installers, or Nix profile packages.
home.packagesDistro packages or Home Manager outside NixOS.
programs.* Home Manager optionsDotfiles under ~/.config or app-native settings.
services.* NixOS optionsNative systemd units and distro service packages.
sops.secrets.*Decrypt with SOPS to files under ~/.local/share/sops.
conf/modules/*Copy selected app config directories into ~/.config.

What Copies Cleanly

ComponentPortable approach
ZellijCopy conf/modules/zellij to ~/.config/zellij.
NeovimClone github:desertthunder/nvim to ~/.config/nvim.
StarshipCopy conf/modules/starship.toml to ~/.config/starship.toml.
ZathuraCopy conf/modules/zathura/zathurarc.
FastfetchCopy conf/modules/fastfetch.
ripgrepRecreate the small config from the program page or source.
SSH keysUse conf/scripts/keys.sh after placing the age key.

What Needs Native Distro Setup

AreaNotes
Desktop sessionInstall GNOME, Hyprland, portals, and login-manager pieces natively.
System servicesEnable Docker, PostgreSQL, Redis, SSH, CUPS, and Tailscale with systemd.
FontsPrefer distro packages; use user font installs for missing Nerd Fonts.
Language toolsUse upstream installers where distro versions lag too far.
SecretsThere is no /run/secrets unless you recreate that pattern yourself.
NixOS aliasesDo not copy aliases that call nixos-rebuild.
  1. Start from the distro’s GNOME edition or another well-supported desktop.
  2. Install baseline CLI tools, editors, fonts, and development services.
  3. Enable Docker, PostgreSQL, Redis, SSH, printing, and Tailscale.
  4. Copy only app configs you actually use.
  5. Extract SSH keys with SOPS if this machine should use repo-managed keys.
  6. Add Nix or Home Manager later only if native packages become too divergent.

Package Strategy

Use native packages for the OS layer: desktop, printing, Bluetooth, networking, Docker, PostgreSQL, Redis, OpenSSH, and Tailscale.

Use language-native installers for fast-moving ecosystems when needed:

ToolchainTypical source
Rustrustup
Bunupstream installer
uvupstream installer
Node/TypeScriptdistro package, pnpm, or project-local tooling
Zedupstream package
Claude CodeNix, upstream package, or npm-style install depending on availability

Use Nix outside NixOS only when it clearly reduces drift. This repo does not currently expose a standalone homeConfigurations.<user> output, so Home Manager outside NixOS would need a small extra flake entry.

Secrets And SSH

NixOS uses SOPS-Nix to mount secrets at /run/secrets. Other distros should use normal user-owned files. The intended portable flow is:

StepResult
Put the age key at ~/.config/sops/age/keys.txtSOPS can decrypt locally.
Run ./conf/scripts/keys.shGit SSH keys land under ~/.local/share/sops.
Point SSH identities at those filesGitHub, Codeberg, and Tangled work.

See Secrets and SSH for current key names and host aliases.

Sanity Checks

After setup, confirm the shape rather than exact package parity:

CheckWhy
Shell is ZshLogin environment matches the dotfiles.
Starship loadsPrompt integration works.
Neovim and Zed startEditors can see expected language tools.
Zellij validates configTerminal workspace config is compatible.
Docker runs a test containerUser is in the Docker group and daemon works.
PostgreSQL accepts a local connectionLocal dev database is ready.
Redis returns PONGLocal cache service is ready.
SSH reaches Git hostsSOPS-extracted keys and aliases are correct.

Expect package substitutions. The goal is the same working environment, not an exact reproduction of NixOS internals.

Dotfiles

This repo can be used as a dotfile source outside NixOS, but only selected pieces are portable. Prefer copying app-native config directories and using Secrets for key extraction.

ConfigPortable source
Zellijconf/modules/zellij
Fastfetchconf/modules/fastfetch
Starshipconf/modules/starship.toml
Zathuraconf/modules/zathura/zathurarc
Neovimgithub:desertthunder/nvim
SSH keysconf/scripts/keys.sh plus SOPS age key

Avoid copying generated Home Manager outputs directly. Copy source config, then let the target machine own package installation and service management.

Programs

These pages document reusable app behavior. They should explain intent, ownership, key settings, and troubleshooting. They should not mirror whole config files or repeat package inventories already visible in Nix source.

For simple tools, prefer summary tables over install scripts. Put exact config in source files and exact Nix internals in Nix Concepts.

Ghostty

Ghostty is the default terminal emulator in my configs.

Home Manager owns the Ghostty settings and GNOME keyboard shortcuts.

Summary

AreaCurrent shape
ShellZsh login shell
Font0xProto Nerd Font
Shell integrationZsh
PaletteDark Carbonfox-like palette
Selectioncopy-on-select = false
Close behaviorNo close confirmation

Launchers

ShortcutCommand
Ctrl-Alt-tghostty
Super-zghostty -e zellij
Hyprland Super-Returnghostty
Hyprland Super-Zghostty -e zellij

Fastfetch

Fastfetch is installed by Home Manager and configured from conf/modules/fastfetch as a quick host, desktop, shell, package, and hardware overview.

Summary

AreaCurrent shape
Config sourceconf/modules/fastfetch
Installed config~/.config/fastfetch

The config is app-native JSONC.

Validate

CheckCommand
Default configfastfetch
Repo configfastfetch --config conf/modules/fastfetch/config.jsonc

Hyprland

Hyprland is intentionally scoped to nix-haxorus. Other hosts keep the shared GNOME/GDM baseline and do not inherit Hyprland, Waybar, rofi, mako, or related user services.

Session Model

Hyprland uses UWSM. In GDM, choose the Hyprland (uwsm-managed) session.

UWSM wraps the compositor as a systemd-managed Wayland session. That gives cleaner environment propagation, XDG autostart behavior, and shutdown handling than launching the compositor as a plain process.

Companion Apps

AppRole
GhosttyDefault terminal.
ZellijTerminal workspace launched through Ghostty.
rofiApp and run launcher.
WaybarPanel.
hyprpaperWallpaper service.
hypridleIdle handling.
hyprlockLock screen.
makoNotifications.
grim, slurp, sattyScreenshot flow.

Ghostty is shared config, but Hyprland binds it directly with Super-Return. Super-Z launches ghostty -e zellij.

Theme

TokenValue
Background#151516
Surface#181818
Border#2a2a2a
Accent#51a4e7
Inner gap4
Outer gap8
Rounding8

The same palette is used by Hyprland, rofi, and the local Zellij marble theme.

Keybinds

KeyAction
Super-ReturnOpen Ghostty
Super-ZOpen Ghostty with Zellij
Super-BOpen Zen Browser
Super-EOpen Nautilus
Super-R, Super-SpaceOpen rofi app launcher
Super-POpen rofi run launcher
Super-Shift-RReload Hyprland config
Super-Shift-LLock session
Super-QClose focused window
Super-Shift-FToggle centered floating
Super-Shift-GToggle fullscreen
Super-Shift-HToggle fake fullscreen
Super-Shift-JToggle Dwindle split
Super-h/j/k/lMove focus
Super-arrow
Super-Ctrl-h/j/k/lResize focused window
Super-Alt-h/j/k/lMove focused window
Super-1..0Switch to workspace 1..10
Super-Shift-1..0Move window to workspace 1..10
Super-SToggle scratch workspace
Super-Shift-SMove window to scratch workspace
Super-mouse1Drag window
Super-mouse2Resize window
PrintRegion screenshot with editor
Shift-PrintFull screenshot with editor
Ctrl-PrintRegion screenshot, no editor
Ctrl-Shift-PrintFull screenshot, no editor

Media keys control PipeWire volume, brightness, and playerctl.

Screenshot paths save to ~/Pictures/Screenshots, copy to clipboard, and notify.

References

  • Hyprland wiki: NixOS
  • Hyprland wiki: Home Manager
  • Hyprland wiki: Systemd startup

Zellij

Summary

AreaCurrent shape
Config sourceconf/modules/zellij/config.kdl
Layoutsconf/modules/zellij/layouts
Themesconf/modules/zellij/themes
Default modeLocked
Thememarble

Layouts

LayoutNotes
defaultSingle pane plus compact bar.
classicLarge left pane, two stacked right panes, compact bar.
ideStrider file explorer, Neovim pane, execution and VCS panes.
ide-stackNeovim-style top pane with a lower terminal pane.
ide-stack-2Neovim-style pane, side pane, and testing pane.

Keys

Shared config starts in locked mode.

Press Ctrl-g to toggle locked and normal mode.

KeyAction
Ctrl-pPane mode
Ctrl-tTab mode
Ctrl-sScroll mode
Ctrl-nResize mode
Ctrl-oSession mode
Ctrl-hMove mode
p, t, s, r, o, mModal shortcuts after unlocking
Alt-h/j/k/lMove focus
Alt-[, Alt-]Cycle swap layouts
Alt-nNew pane
Alt-fToggle floating panes

Existing sessions do not reliably pick up keymap edits. Start a fresh Zellij server/session after changing config.kdl.

Validate

CheckCommand
Binaryzellij --version
Active configzellij setup --check
Repo configSet ZELLIJ_CONFIG_FILE then run zellij setup --check

Zed

Zed is managed through Home Manager and installed from nixpkgs-unstable so the editor can move faster than the system channel.

Extensions

Extensions are declared in programs.zed-editor.extensions. Keep that list in source rather than duplicating it here; it changes more often than the operating model.

The important rule is that registry themes must have both pieces:

NeedExample
Extension installedcarbonfox
Theme selectedCarbonfox - opaque

Zathura

Zathura is my PDF reader of choice.

Home Manager installs Zathura, the Poppler backend, and the native zathurarc.

Summary

AreaCurrent shape
Config sourceconf/modules/zathura/zathurarc
Backendzathura_pdf_poppler
ThemeDark background, light foreground, blue highlights
InterfaceStatus/input bars hidden; page padding and recolor enabled

Mappings

KeyAction
uScroll half page up
dScroll half page down
iRecolor
pPrint
rReload
RRotate
K / JZoom in / out
fToggle fullscreen
qQuit

Validate

CheckCommand
Binaryzathura --version
BackendOpen a PDF

Neovim

Home Manager enables Neovim, sets it as the default editor, and copies the external Neovim config from the neovim-config flake input.

Summary

AreaCurrent shape
Config sourcegithub:desertthunder/nvim flake input
Installed config~/.config/nvim
Aliasesvi, vim
Default editorNeovim
Provider supportPython 3 and Ruby enabled
ToolingLanguage servers live in editor-tool-pkgs

Workflow

Update editor behavior in the Neovim config repo. Update this repo when the flake input should move to a newer revision or when system language tools need to change.

Validate

CheckCommand
Binarynvim --version
Config healthnvim '+checkhealth'
Default editorecho "$EDITOR"

Git

Home Manager configures Git identity and a global ignore list.

Summary

SettingValue
Ignore sourceprograms.git.ignores in conf/shared.nix
SSH configSSH
Secret extractionSecrets

Ignore Policy

The global ignore list covers OS/editor junk, local env files, Nix build results, direnv/devenv state, sandbox output, and local agent files such as AGENTS.md, CLAUDE.md, and .claude/settings.local.json.

Validate

CheckCommand
Namegit config --global --get user.name
Emailgit config --global --get user.email
SSH authssh -T [email protected]

Zsh

Zsh has been my go-to shell for the past decade.

Home Manager adds completion, autosuggestions, syntax highlighting, Oh My Zsh, Starship initialization, and a small alias set.

Summary

AreaCurrent shape
Login shellpkgs.zsh from NixOS user config
Oh My Zsh pluginsgit, z
PromptStarship initialized from zsh init
PATH additions~/.local/bin, ~/.cargo/bin, ~/go/bin
NixOS config pathNIXOS_CONFIG, defaulting to ~/Projects/nixos-conf

Aliases

AliasPurpose
llLong ls.
catbat without paging or decorations.
lessbat pager.
previewbat with numbers and change markers.
zed, zednZed shortcuts.
rebuild, switch, update, nboot, tbuildNixOS rebuild helpers.

AI Agents

Project-local skills are documented in Agent Skills.

Codex

Login in through the TUI.

Claude Code

Home Manager installs Claude Code and writes ~/.claude/settings.json. Claude Code uses the Umans Anthropic-compatible API:

https://api.code.umans.ai/v1/messages

The base URL in config is https://api.code.umans.ai; Claude Code appends the Anthropic API path itself.

Authentication comes from apiKeyHelper, which reads:

/run/secrets/umans_key

This keeps the API key out of the Nix store.

Ownership

  • conf/shared.nix: pkgsUnstable.claude-code
  • conf/shared.nix: home.file.".claude/settings.json"
  • conf/shared.nix: sops.secrets.umans_key
  • conf/secrets/owais.yaml: encrypted umans_key

Models

Default model:

umans-glm-5.2

Fallback model:

umans-coder

The settings map Claude Code's model aliases to Umans models:

  • fable, opus, and sonnet: umans-glm-5.2
  • haiku: umans-coder

Gateway model discovery is enabled. No external search provider is configured; web search should come from the model/provider if Umans exposes it natively.

Validate

After rebuilding, open a fresh shell and run:

claude doctor
claude

Inside Claude Code, run:

/status
/model

/status should show the Umans base URL.

/model should show the configured models or the aliases mapped to those models.

Agent Skills

Project-local skills live under conf/agent/skills/. Each skill is a directory with a SKILL.md file and optional references/ files.

How skills are triggered

Skills are selected by the agent, not by a shell command. The harness exposes the frontmatter from each SKILL.md:

---
name: css
description: Write, refactor, and review well-structured vanilla CSS...
---

The agent reads the available skill names and descriptions, then loads the skill when the user's request matches. Good descriptions matter: they should name the work, common synonyms, and when to use the skill.

Examples:

  • "Refactor this Tailwind into plain CSS" should trigger css.
  • "Make this memo sound less AI-generated" should trigger writing.
  • "Turn this idea into a PRD" should trigger spec-writing.
  • "Take notes from this article" should trigger notetaking.
  • "Add SvelteKit form action tests" should trigger svelte-testing.
  • "Audit this repo before changing it" should trigger investigate-new-codebase.

Reference files are not loaded automatically unless the skill tells the agent to read them. Keep the main SKILL.md small enough to load quickly and move long examples, templates, and catalogs into references/.

Current skills

SkillUse it for
cssVanilla CSS structure, component classes, tokens, accessible colours, and replacing utility CSS.
investigate-new-codebaseFirst-pass repo audits, legacy rescue, risk mapping, git-history analysis, and triage.
notetakingTurning articles, Markdown, or web pages into concise notes with claims, evidence, and review questions.
spec-writingSpecs, PRDs, implementation plans, task breakdowns, acceptance criteria, and agent instructions.
svelte-testingSvelte and SvelteKit unit, component, server, SSR, browser, and E2E testing.
writingDrafting, revising, editing, and desloping prose in a direct human voice.

Installation pattern

The source of truth is always this repository:

conf/agent/skills/<skill>/SKILL.md
conf/agent/skills/<skill>/references/

On NixOS, Home Manager publishes the project skills to Pi:

home.file.".pi/agent/skills" = {
  source = ./agent/skills;
  recursive = true;
  force = true;
};

For generic agent harnesses that read ~/.agents/skills, expose project skills with symlinks rather than copies:

mkdir -p ~/.agents/skills/css
ln -s "$PWD/conf/agent/skills/css/SKILL.md" ~/.agents/skills/css/SKILL.md
ln -s "$PWD/conf/agent/skills/css/references" ~/.agents/skills/css/references

Do not keep duplicate standalone skills in ~/.agents/skills or ~/.pi/agent/skills when their guidance has been consolidated into this repo. Duplicates drift and make it unclear which behavior the agent should follow.

Self-updating pattern

A self-updating skill is a small feedback loop:

  1. Use the skill. Let it guide real work.
  2. Notice a reusable lesson. Prefer repeated feedback, failed outputs, or a missing verification step over one-off preferences.
  3. Distill the lesson. Add the smallest concrete rule that would prevent the same issue next time.
  4. Choose the right file. Put short operating rules in SKILL.md; put long examples, templates, catalogs, and source notes in references/.
  5. Review the change. Human review matters because a bad skill update affects every future use.
  6. Let symlinks publish it. Since ~/.agents points back to this repo, the next agent run sees the update without copying files.

Each self-updating skill should include a ## Self-update section that says when to update it and what not to add.

Good updates:

  • Add a missing checklist item after a spec causes implementation drift.
  • Add a prose trope after the user repeatedly removes it.
  • Add a CSS rule after the same layout mistake appears in several components.

Bad updates:

  • Add project-specific facts that belong in project docs.
  • Add every user preference after one comment.
  • Add long examples directly to SKILL.md when a reference file would keep the trigger lightweight.

Lessons from MindStudio's self-improving skill system

MindStudio's article on self-improving AI skill systems describes four useful parts: a narrow skill library, shared context, eval scoring, and a learnings loop. For this repo, the practical translation is:

  • Keep skills narrow, reusable, and easy to evaluate.
  • Store shared context in versioned files, not hidden chat history.
  • Score outputs with concrete checks when possible: tests pass, contrast is readable, the spec has verification steps, the prose avoids known tells.
  • Write learnings back only after review.
  • Track where a learning came from, so bad guidance can be rolled back.
  • Cap updates. A few high-signal rules beat a growing pile of noisy advice.
  • Avoid context bloat. Promote durable lessons; leave one-off details out.

The article's fully automated loop is useful as a model, but this repository uses a human-reviewed version. The agent may propose or make a skill update, but the change should be concrete, versioned, and easy to inspect.

Source: https://www.mindstudio.ai/blog/self-improving-ai-skill-system-marketing-content

Starship

Starship is the shell prompt. Home Manager installs the binary, copies the native TOML config, and Zsh initializes it.

Summary

AreaCurrent shape
Config sourceconf/modules/starship.toml
Installed config~/.config/starship.toml
Shell integrationeval "$(starship init zsh)"

Prompt Shape

The config keeps the prompt compact and development-focused. It emphasizes directory, Git state, language/runtime context, command duration, jobs, status, and shell character.

For portable use, install Starship and copy the TOML config. The only shell requirement is that Zsh initializes Starship.

Validate

CheckCommand
Binarystarship --version
Render configstarship explain

ripgrep

ripgrep is installed by Home Manager and gets a small default config at ~/.config/ripgrep/config.

Defaults

SettingPurpose
--line-numberShow file line numbers.
--smart-caseCase-insensitive search unless the pattern has capitals.
--max-columns=120Avoid unreadably long output lines.
--max-columns-previewStill show a preview for long lines.
--type-add=nix:*.nixTeach ripgrep about Nix files.
--glob=!.git/*Skip Git internals.
--glob=!**/node_modules/**Skip JS dependencies.
--glob=!**/target/**Skip Rust build output.
--glob=!**/.build/**Skip common build output.

Usage

General search examples live in Tools.

This page only documents the repo default behavior.

SSH

Home Manager writes SSH host entries for GitHub, Codeberg, Tangled, Forgejo, and the Tangled Knot. On NixOS, identities come from SOPS-Nix paths under /run/secrets.

Host Aliases

HostUserIdentityPurpose
github.comgitkeys_ghGitHub remotes
codeberg.orggitkeys_codebergCodeberg remotes
tangled.shgitkeys_tangledTangled hosted remotes
knot.desertthunder.devgitkeys_tangledKnot host
nix-baxcalibur-knotgitkeys_tangledTailnet Tangled Knot remotes

The shared config sets IdentitiesOnly yes and AddKeysToAgent no.

Secret Paths

EnvironmentPath style
NixOS/run/secrets/keys_*
Non-NixOS~/.local/share/sops/keys_*

See Secrets for extraction and permissions. This page should not duplicate the SOPS workflow.

Validate

CheckCommand
GitHub authssh -T [email protected]
Codeberg authssh -T [email protected]
Tangled authssh -T [email protected]
Knot over tailnetssh -T nix-baxcalibur-knot
Debug identity choicessh -vT [email protected]

When debugging, look for Offering public key and confirm the path matches the configured identity.

Obsidian

Obsidian is installed for owais by Home Manager from conf/shared.nix.

The shared package set applies to every NixOS host in this flake, so both nix-haxorus and nix-baxcalibur receive the desktop app after rebuild.

Verify:

obsidian --version

Vault sync is planned to use the private Forgejo repository on Baxcalibur over Tailscale SSH. Keep device-local Obsidian state out of that repository with a vault .gitignore.

Tools

ripgrep

Use rg first for repository search. The default config is documented on the ripgrep program page.

Common searches:

rg pattern
rg -i pattern
rg "fn name" -t nix
rg pattern path/to/dir
rg -n -C 2 pattern
rg --files
rg --files -g '*.nix'

Filtering:

rg pattern -g '*.nix'
rg pattern -g '!**/node_modules/**'
rg pattern --hidden -g '!.git'
rg pattern -t md

Output:

rg pattern --json
rg pattern --count
rg pattern --files-with-matches
rg pattern --replace replacement

Project defaults:

--line-number
--smart-case
--max-columns=120
--max-columns-preview
--type-add=nix:*.nix
--glob=!.git/*
--glob=!**/node_modules/**
--glob=!**/target/**
--glob=!**/.build/**

OCaml

Home Manager installs opam, ocaml, dune, utop, ocamlformat, and the OCaml language server.

For a new switch:

opam init
opam switch create . ocaml-base-compiler.5.4.1
opam install . --deps-only --with-test --with-doc

Other CLI tools

Home Manager installs common helpers including fd, jq, yq, fzf, bat, dust, tree, zellij, gum, glow, vhs, btop, cava, shellcheck, shfmt, zig, zls, lldb, typst, and mdbook.

TODO Comment Search

Use ripgrep (rg) to find TODO-style comments quickly across projects. This is useful for local cleanup, release checks, and CI gates.

rg --no-messages --vimgrep -H --column --line-number --color never \
  -e '(TODO|FIXME|BUG|HACK|XXX)' .

--vimgrep, -H, --column, and --line-number produce rows in this form:

path/to/file:line:column:matched text

This pattern looks for common comment prefixes, markdown task/list items, or a tag at the start of a line:

TAGS='BUG|HACK|FIXME|TODO|XXX|\[ \]|\[x\]'
PREFIX='//|#|<!--|;|/\*|^|^[[:blank:]]*(-|[0-9]+\.)'

rg --no-messages --vimgrep -H --column --line-number --color never \
  --max-columns=1000 --no-config \
  -e "(${PREFIX})[[:space:]]*(${TAGS})" \
  -g '!**/.git/**' \
  -g '!**/node_modules/**' \
  -g '!**/target/**' \
  -g '!**/.build/**' \
  .

Notes:

  • --no-config keeps personal rg config from changing project results.
  • --max-columns=1000 avoids dropping long lines too early.
  • Add -i for case-insensitive tags.
  • Add --hidden when dotfiles should be scanned too.

Project script

Add this as scripts/todos:

#!/usr/bin/env bash
set -euo pipefail

TAGS='BUG|HACK|FIXME|TODO|XXX|\[ \]|\[x\]'
PREFIX='//|#|<!--|;|/\*|^|^[[:blank:]]*(-|[0-9]+\.)'

rg --no-messages --vimgrep -H --column --line-number --color never \
  --max-columns=1000 --no-config \
  -e "(${PREFIX})[[:space:]]*(${TAGS})" \
  -g '!**/.git/**' \
  -g '!**/node_modules/**' \
  -g '!**/target/**' \
  -g '!**/.build/**' \
  "$@" \
  .

Then:

chmod +x scripts/todos
scripts/todos

Filters

Use -g for include and exclude globs. A glob beginning with ! excludes matches.

# Only source and markdown paths
scripts/todos -g 'src/**' -g 'docs/**' -g '*.md'

# Exclude generated/vendor paths
scripts/todos -g '!**/vendor/**' -g '!**/dist/**' -g '!**/*.lock'

# Include hidden files
scripts/todos --hidden

CI check

Fail when TODO-style comments are present:

if scripts/todos >/tmp/todos.txt; then
  cat /tmp/todos.txt
  echo "TODO comments found"
  exit 1
fi

rg exits with 0 when it finds a match and 1 when it finds none.

Resources

Programming Fonts is a good way to try out new fonts.