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 + 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.
| Area | Current choice |
|---|---|
| System | NixOS or Fedora, flakes, Home Manager, and host-specific modules. |
| DE | GNOME, Hyprland, or XFCE (machine dependent) |
| Term | Ghostty, usually running Zellij. |
| Shell | Zsh with Starship. |
| Editor | Neovim & Zed (vim-mode) |
| Notes | Neovim & Obsidian |
| Reading | Zathura with a compact dark interface. |
| Networking | Tailscale for stable hostnames and private services. |
Entrypoints
- Guides: common commands, checks, secrets, and migration notes
- Nix Concepts
- NixOS: flakes, hosts, and SOPS
- Programs: Dotfiles
- Other Distros: how to recreate the working setup without NixOS
- Tools: small command-line notes and scripts
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-instantiatenormally 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 FILEis 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.--parsedoes not evaluate imports, module options, flake outputs, package names, paths, assertions, or type checks. A file can parse successfully and still fail duringnix flake showornixos-rebuild.nix flake showonly evaluates the flake; it does not build anything.nixos-rebuild build/testwill actually evaluate/build the system closure and will catch missing packages/options earlier thanswitch.
Troubleshooting
- Use
sudo nixos-rebuild test --flake .#hostbefore switching. - If Home Manager reports file collisions, move the existing file and rebuild.
- If secrets fail, confirm
/var/lib/sops-nix/key.txtexists on NixOS or setSOPS_AGE_KEY_FILEmanually 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.
| Topic | Use it for |
|---|---|
| Language basics | Reading Nix expressions and small snippets. |
| Flakes | Understanding inputs, outputs, and the lock file. |
| Modules | Understanding how NixOS and Home Manager compose settings. |
| NixOS and Home Manager | How 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.
| Form | Meaning |
|---|---|
"hello" | String |
./file.nix | Path, copied into the Nix store when referenced |
[ a b c ] | List |
{ key = value; } | Attribute set |
x: x + 1 | Function |
{ pkgs, ... }: { } | Function taking an attribute set |
let name = value; in body | Local 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.
| Part | Role |
|---|---|
inputs.nixpkgs | Main Nixpkgs channel for systems and packages. |
inputs.nixpkgs-unstable | Fast-moving packages, currently used for Zed & Claude Code. |
inputs.home-manager | User environment as part of each NixOS system. |
inputs.sops-nix | Secret material decrypted onto NixOS hosts. |
outputs.nixosConfigurations | Buildable 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
| Concept | Meaning |
|---|---|
imports | Adds more modules to the current module. |
| Options | Declared settings such as services.openssh.enable. |
| Values | Assignments to options. |
| Merge | The module system combines compatible values and reports conflicts. |
lib.mkIf | Adds settings conditionally. |
specialArgs | Extra 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.
| Layer | Owns |
|---|---|
| NixOS | boot, users, system services, hardware, system packages, secrets |
| Home Manager | dotfiles, user packages, editor config, shell config, desktop user config |
| Host modules | machine-specific hardware and opt-in services |
| Shared modules | baseline 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 = trueviAlias,vimAlias, anddefaultEditorare 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-confighere 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.
| Host | Machine directory | Notes |
|---|---|---|
nix-haxorus | conf/machines/thinkpad/ | ThinkPad workstation with Hyprland. |
nix-baxcalibur | conf/machines/hp/ | HP host for shared services. |
Repository Layout
| Path | Purpose |
|---|---|
flake.nix | Host outputs, inputs, and Home Manager wiring. |
flake.lock | Exact upstream revisions. |
conf/shared.nix | Shared NixOS and Home Manager modules. |
conf/machines/*/configuration.nix | Host-specific system choices. |
conf/machines/*/hardware-configuration.nix | Generated hardware facts. |
conf/modules/ | App config assets and focused local modules. |
conf/services/ | Reusable service modules. |
conf/secrets/owais.yaml | SOPS-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.nixconf/machines/thinkpad/hardware-configuration.nixconf/modules/de/hypr.nixconf/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-daemondisabled- default TLP mode set to battery
- CPU governor set to
performanceon AC andpowersaveon battery - fingerprint daemon enabled
- Hyprland desktop enabled through host-specific modules
nix-baxcalibur
HP configuration.
Files:
conf/machines/hp/configuration.nixconf/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
| Secret | NixOS path | Used by |
|---|---|---|
keys_gh | /run/secrets/keys_gh | GitHub SSH |
keys_codeberg | /run/secrets/keys_codeberg | Codeberg SSH |
keys_tangled | /run/secrets/keys_tangled | Tangled and Knot SSH |
umans_key | /run/secrets/umans_key | Claude Code Umans API |
Files are owned by owais:users with mode 0600.
NixOS Model
| Piece | Role |
|---|---|
conf/secrets/owais.yaml | Encrypted secret values. |
.sops.yaml | Recipient and file encryption policy. |
/var/lib/sops-nix/key.txt | Age key used by NixOS during activation. |
conf/shared.nix | Declares 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 input | Output |
|---|---|
~/.config/sops/age/keys.txt | Local age key |
conf/secrets/owais.yaml | Encrypted source |
~/.local/share/sops/keys_gh | GitHub key |
~/.local/share/sops/keys_codeberg | Codeberg key |
~/.local/share/sops/keys_tangled | Tangled 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
| Symptom | Likely cause |
|---|---|
| SOPS cannot decrypt locally | SOPS_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 key | File 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:
| Name | Example | Used for |
|---|---|---|
| Directory | framework, x1 | Human-readable folder under conf/machines/. |
| Hostname | haxorus | networking.hostName and flake target. |
They may match, but they do not have to.
Existing hosts use short hardware directory names and Pokemon hostnames.
Checklist
| Step | File or command | Notes |
|---|---|---|
| Generate hardware config | nixos-generate-config --show-hardware-config | Store it in the new host directory. |
| Add host module | conf/machines/{machine}/configuration.nix | Import hardware and shared NixOS config. |
| Set hostname | networking.hostName = "{hostname}"; | Must match the flake target you plan to build. |
| Add flake output | nixosConfigurations.{hostname} | Copy the shape of existing hosts. |
| Choose architecture | x86_64-linux or aarch64-linux | Match the target CPU. |
| Build temporarily | sudo nixos-rebuild test --flake .#{hostname} | Safer first activation. |
| Make persistent | sudo 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. testsucceeded beforeswitch.
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 area | Baseline |
|---|---|
| Tailscale | Enabled on every NixOS host with firewall support. |
| OpenSSH | Enabled on every NixOS host. |
| PostgreSQL | Local development database baseline. |
| Redis | Local development cache baseline. |
| Docker | Local container runtime baseline. |
Service Pages
| Page | Scope |
|---|---|
| Tailscale | Private network, MagicDNS, Serve, and tailnet policy. |
| Git Forge | Forgejo on Baxcalibur. |
| Kavita | Comics, manga, ebooks, and PDFs on Baxcalibur. |
| Tangled Knot | Tangled 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
| Layer | Owns |
|---|---|
conf/shared.nix | Enables tailscaled, opens firewall, installs CLI. |
| Tailscale admin console | MagicDNS, HTTPS, device approval, expiry, users, groups, ACLs. |
| Service pages | How individual services use tailnet access. |
How This Repo Uses It
| Use | Policy |
|---|---|
| Hostnames | Prefer MagicDNS names such as nix-baxcalibur. |
| Git writes | Use SSH over the tailnet. |
| Forgejo private/admin access | Keep on the tailnet. |
| Kavita | Publish private HTTPS through Tailscale Serve. |
| Public exposure | Use 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
| Check | Command |
|---|---|
| Daemon | systemctl status tailscaled |
| Tailnet state | tailscale status |
| Local tailnet IP | tailscale ip |
| MagicDNS | tailscale ip nix-baxcalibur |
| SSH path | ssh owais@nix-baxcalibur |
| Serve routes | tailscale 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.
| Service | Local endpoint | Exposure |
|---|---|---|
| Kavita | 127.0.0.1:5000 | Tailscale Serve |
| Forgejo admin/private use | 127.0.0.1:3030 | Tailnet first |
| Tangled Knot SSH | system SSH on 22 | Tailnet SSH path |
Funnel is public internet exposure. Treat it as temporary or explicitly intentional service policy, not the default.
Troubleshooting
| Symptom | Check |
|---|---|
| Name does not resolve | MagicDNS enabled, client on correct tailnet, device online. |
| SSH hangs | ACLs, MagicDNS result, and target sshd. |
| Serve URL missing | Tailnet HTTPS enabled and Serve route configured. |
| Device repeatedly expires | Admin console expiry policy for that node. |
References
- Tailscale Linux install: https://tailscale.com/docs/install/linux
- Tailscale CLI: https://tailscale.com/docs/reference/tailscale-cli
- MagicDNS: https://tailscale.com/docs/features/magicdns
- Tailscale Serve: https://tailscale.com/docs/features/tailscale-serve
- Access controls: https://tailscale.com/docs/features/access-control/acls
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
| Area | Value |
|---|---|
| Module | conf/services/forgejo.nix |
| Host | nix-baxcalibur |
| Local URL | 127.0.0.1:3030 |
| Public URL | https://git.desertthunder.dev/ |
| Database | Local PostgreSQL |
| State | /var/lib/forgejo |
| Public ingress | Cloudflare Tunnel |
| Private control plane | Tailscale |
| Registration | Disabled |
Boundaries
| Path | Policy |
|---|---|
| Public HTTPS reads | Cloudflare Tunnel to local Forgejo. |
| Writes | SSH over the tailnet. |
| Admin/private repositories | Tailnet first. |
| LFS writes | Tailnet path to avoid Cloudflare free-tier limits. |
| SSH port | Shared system port 22, separated by SSH user. |
The Cloudflare tunnel should only route the intended hostname and return 404
for unmatched hostnames.
Client Model
| Client | Expected path |
|---|---|
| Desktop | Normal Git over SSH to Baxcalibur’s tailnet name. |
| Android/Termux | Git and OpenSSH over Tailscale. |
| Obsidian vault | Repository-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:
| Field | Value |
|---|---|
| Host | nix-baxcalibur |
| Port | 22 |
| User | forgejo |
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
| Check | Command |
|---|---|
| Service | systemctl status forgejo |
| Recent logs | journalctl -u forgejo -e |
| Tailnet SSH | ssh -T nix-baxcalibur-forgejo |
| Remote shape | git 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
- Forgejo: https://forgejo.org/docs/latest/admin/config-cheat-sheet/
- NixOS module: https://github.com/NixOS/nixpkgs/blob/nixos-26.05/nixos/modules/services/misc/forgejo.nix
- Forgejo customization: https://forgejo.org/docs/latest/admin/advanced/customization/
- Cloudflare Tunnel: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/
- Tailscale: https://tailscale.com/docs/concepts/tailnet
- Termux: https://termux.dev/
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
- Tangled knot self-hosting guide: https://docs.tangled.org/knot-self-hosting-guide#nixos
- Upstream Knot NixOS module: https://tangled.org/tangled.org/core/blob/master/nix/modules/knot.nix
- Tangled flake: https://tangled.org/@tangled.org/core
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=1GCPUQuota=150%IOSchedulingClass=best-effortIOSchedulingPriority=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:
- Install Tailscale.
- Sign in to the same tailnet.
- Open the Kavita HTTPS URL from
tailscale serve status. - 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
- Kavita: https://github.com/Kareadita/Kavita
- Kavita docs: https://wiki.kavitareader.com/getting-started/
- NixOS module: https://github.com/NixOS/nixpkgs/blob/nixos-26.05/nixos/modules/services/web-apps/kavita.nix
- Tailscale Serve: https://tailscale.com/docs/reference/tailscale-cli/serve
- Tailscale Funnel: https://tailscale.com/docs/features/tailscale-funnel
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
| Area | Expected result |
|---|---|
| Desktop | GNOME or Hyprland on Wayland, PipeWire, NetworkManager, printing, SSH. |
| Shell | Zsh login shell with Starship and a small alias set. |
| Editors | Neovim and Zed with language servers available on PATH. |
| Terminal | Ghostty, usually launching Zsh and sometimes Zellij. |
| Dev services | Docker, PostgreSQL, and Redis for local development. |
| CLI tools | ripgrep, fd, jq/yq, fzf, bat, direnv, just, and common compilers. |
| Fonts | Nerd fonts plus Inter, Open Sans, Noto, and other UI/code fonts. |
| Secrets | SOPS-extracted SSH keys under a normal user-owned path. |
Translation Map
| NixOS source | Portable equivalent |
|---|---|
environment.systemPackages | Distro packages, language installers, or Nix profile packages. |
home.packages | Distro packages or Home Manager outside NixOS. |
programs.* Home Manager options | Dotfiles under ~/.config or app-native settings. |
services.* NixOS options | Native 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
| Component | Portable approach |
|---|---|
| Zellij | Copy conf/modules/zellij to ~/.config/zellij. |
| Neovim | Clone github:desertthunder/nvim to ~/.config/nvim. |
| Starship | Copy conf/modules/starship.toml to ~/.config/starship.toml. |
| Zathura | Copy conf/modules/zathura/zathurarc. |
| Fastfetch | Copy conf/modules/fastfetch. |
| ripgrep | Recreate the small config from the program page or source. |
| SSH keys | Use conf/scripts/keys.sh after placing the age key. |
What Needs Native Distro Setup
| Area | Notes |
|---|---|
| Desktop session | Install GNOME, Hyprland, portals, and login-manager pieces natively. |
| System services | Enable Docker, PostgreSQL, Redis, SSH, CUPS, and Tailscale with systemd. |
| Fonts | Prefer distro packages; use user font installs for missing Nerd Fonts. |
| Language tools | Use upstream installers where distro versions lag too far. |
| Secrets | There is no /run/secrets unless you recreate that pattern yourself. |
| NixOS aliases | Do not copy aliases that call nixos-rebuild. |
Recommended Path
- Start from the distro’s GNOME edition or another well-supported desktop.
- Install baseline CLI tools, editors, fonts, and development services.
- Enable Docker, PostgreSQL, Redis, SSH, printing, and Tailscale.
- Copy only app configs you actually use.
- Extract SSH keys with SOPS if this machine should use repo-managed keys.
- 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:
| Toolchain | Typical source |
|---|---|
| Rust | rustup |
| Bun | upstream installer |
| uv | upstream installer |
| Node/TypeScript | distro package, pnpm, or project-local tooling |
| Zed | upstream package |
| Claude Code | Nix, 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:
| Step | Result |
|---|---|
Put the age key at ~/.config/sops/age/keys.txt | SOPS can decrypt locally. |
Run ./conf/scripts/keys.sh | Git SSH keys land under ~/.local/share/sops. |
| Point SSH identities at those files | GitHub, 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:
| Check | Why |
|---|---|
| Shell is Zsh | Login environment matches the dotfiles. |
| Starship loads | Prompt integration works. |
| Neovim and Zed start | Editors can see expected language tools. |
| Zellij validates config | Terminal workspace config is compatible. |
| Docker runs a test container | User is in the Docker group and daemon works. |
| PostgreSQL accepts a local connection | Local dev database is ready. |
Redis returns PONG | Local cache service is ready. |
| SSH reaches Git hosts | SOPS-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.
| Config | Portable source |
|---|---|
| Zellij | conf/modules/zellij |
| Fastfetch | conf/modules/fastfetch |
| Starship | conf/modules/starship.toml |
| Zathura | conf/modules/zathura/zathurarc |
| Neovim | github:desertthunder/nvim |
| SSH keys | conf/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
| Area | Current shape |
|---|---|
| Shell | Zsh login shell |
| Font | 0xProto Nerd Font |
| Shell integration | Zsh |
| Palette | Dark Carbonfox-like palette |
| Selection | copy-on-select = false |
| Close behavior | No close confirmation |
Launchers
| Shortcut | Command |
|---|---|
Ctrl-Alt-t | ghostty |
Super-z | ghostty -e zellij |
Hyprland Super-Return | ghostty |
Hyprland Super-Z | ghostty -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
| Area | Current shape |
|---|---|
| Config source | conf/modules/fastfetch |
| Installed config | ~/.config/fastfetch |
The config is app-native JSONC.
Validate
| Check | Command |
|---|---|
| Default config | fastfetch |
| Repo config | fastfetch --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
| App | Role |
|---|---|
| Ghostty | Default terminal. |
| Zellij | Terminal workspace launched through Ghostty. |
| rofi | App and run launcher. |
| Waybar | Panel. |
| hyprpaper | Wallpaper service. |
| hypridle | Idle handling. |
| hyprlock | Lock screen. |
| mako | Notifications. |
| grim, slurp, satty | Screenshot flow. |
Ghostty is shared config, but Hyprland binds it directly with
Super-Return. Super-Z launches ghostty -e zellij.
Theme
| Token | Value |
|---|---|
| Background | #151516 |
| Surface | #181818 |
| Border | #2a2a2a |
| Accent | #51a4e7 |
| Inner gap | 4 |
| Outer gap | 8 |
| Rounding | 8 |
The same palette is used by Hyprland, rofi, and the local Zellij marble
theme.
Keybinds
| Key | Action |
|---|---|
Super-Return | Open Ghostty |
Super-Z | Open Ghostty with Zellij |
Super-B | Open Zen Browser |
Super-E | Open Nautilus |
Super-R, Super-Space | Open rofi app launcher |
Super-P | Open rofi run launcher |
Super-Shift-R | Reload Hyprland config |
Super-Shift-L | Lock session |
Super-Q | Close focused window |
Super-Shift-F | Toggle centered floating |
Super-Shift-G | Toggle fullscreen |
Super-Shift-H | Toggle fake fullscreen |
Super-Shift-J | Toggle Dwindle split |
Super-h/j/k/l | Move focus |
Super-arrow | |
Super-Ctrl-h/j/k/l | Resize focused window |
Super-Alt-h/j/k/l | Move focused window |
Super-1..0 | Switch to workspace 1..10 |
Super-Shift-1..0 | Move window to workspace 1..10 |
Super-S | Toggle scratch workspace |
Super-Shift-S | Move window to scratch workspace |
Super-mouse1 | Drag window |
Super-mouse2 | Resize window |
Print | Region screenshot with editor |
Shift-Print | Full screenshot with editor |
Ctrl-Print | Region screenshot, no editor |
Ctrl-Shift-Print | Full 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
| Area | Current shape |
|---|---|
| Config source | conf/modules/zellij/config.kdl |
| Layouts | conf/modules/zellij/layouts |
| Themes | conf/modules/zellij/themes |
| Default mode | Locked |
| Theme | marble |
Layouts
| Layout | Notes |
|---|---|
default | Single pane plus compact bar. |
classic | Large left pane, two stacked right panes, compact bar. |
ide | Strider file explorer, Neovim pane, execution and VCS panes. |
ide-stack | Neovim-style top pane with a lower terminal pane. |
ide-stack-2 | Neovim-style pane, side pane, and testing pane. |
Keys
Shared config starts in locked mode.
Press Ctrl-g to toggle locked and normal mode.
| Key | Action |
|---|---|
Ctrl-p | Pane mode |
Ctrl-t | Tab mode |
Ctrl-s | Scroll mode |
Ctrl-n | Resize mode |
Ctrl-o | Session mode |
Ctrl-h | Move mode |
p, t, s, r, o, m | Modal shortcuts after unlocking |
Alt-h/j/k/l | Move focus |
Alt-[, Alt-] | Cycle swap layouts |
Alt-n | New pane |
Alt-f | Toggle floating panes |
Existing sessions do not reliably pick up keymap edits. Start a fresh Zellij
server/session after changing config.kdl.
Validate
| Check | Command |
|---|---|
| Binary | zellij --version |
| Active config | zellij setup --check |
| Repo config | Set 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:
| Need | Example |
|---|---|
| Extension installed | carbonfox |
| Theme selected | Carbonfox - opaque |
Zathura
Zathura is my PDF reader of choice.
Home Manager installs Zathura, the Poppler backend, and the native zathurarc.
Summary
| Area | Current shape |
|---|---|
| Config source | conf/modules/zathura/zathurarc |
| Backend | zathura_pdf_poppler |
| Theme | Dark background, light foreground, blue highlights |
| Interface | Status/input bars hidden; page padding and recolor enabled |
Mappings
| Key | Action |
|---|---|
u | Scroll half page up |
d | Scroll half page down |
i | Recolor |
p | |
r | Reload |
R | Rotate |
K / J | Zoom in / out |
f | Toggle fullscreen |
q | Quit |
Validate
| Check | Command |
|---|---|
| Binary | zathura --version |
| Backend | Open 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
| Area | Current shape |
|---|---|
| Config source | github:desertthunder/nvim flake input |
| Installed config | ~/.config/nvim |
| Aliases | vi, vim |
| Default editor | Neovim |
| Provider support | Python 3 and Ruby enabled |
| Tooling | Language 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
| Check | Command |
|---|---|
| Binary | nvim --version |
| Config health | nvim '+checkhealth' |
| Default editor | echo "$EDITOR" |
Git
Home Manager configures Git identity and a global ignore list.
Summary
| Setting | Value |
|---|---|
| Ignore source | programs.git.ignores in conf/shared.nix |
| SSH config | SSH |
| Secret extraction | Secrets |
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
| Check | Command |
|---|---|
| Name | git config --global --get user.name |
git config --global --get user.email | |
| SSH auth | ssh -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
| Area | Current shape |
|---|---|
| Login shell | pkgs.zsh from NixOS user config |
| Oh My Zsh plugins | git, z |
| Prompt | Starship initialized from zsh init |
| PATH additions | ~/.local/bin, ~/.cargo/bin, ~/go/bin |
| NixOS config path | NIXOS_CONFIG, defaulting to ~/Projects/nixos-conf |
Aliases
| Alias | Purpose |
|---|---|
ll | Long ls. |
cat | bat without paging or decorations. |
less | bat pager. |
preview | bat with numbers and change markers. |
zed, zedn | Zed shortcuts. |
rebuild, switch, update, nboot, tbuild | NixOS 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-codeconf/shared.nix:home.file.".claude/settings.json"conf/shared.nix:sops.secrets.umans_keyconf/secrets/owais.yaml: encryptedumans_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, andsonnet:umans-glm-5.2haiku: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
| Skill | Use it for |
|---|---|
css | Vanilla CSS structure, component classes, tokens, accessible colours, and replacing utility CSS. |
investigate-new-codebase | First-pass repo audits, legacy rescue, risk mapping, git-history analysis, and triage. |
notetaking | Turning articles, Markdown, or web pages into concise notes with claims, evidence, and review questions. |
spec-writing | Specs, PRDs, implementation plans, task breakdowns, acceptance criteria, and agent instructions. |
svelte-testing | Svelte and SvelteKit unit, component, server, SSR, browser, and E2E testing. |
writing | Drafting, 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:
- Use the skill. Let it guide real work.
- Notice a reusable lesson. Prefer repeated feedback, failed outputs, or a missing verification step over one-off preferences.
- Distill the lesson. Add the smallest concrete rule that would prevent the same issue next time.
- Choose the right file. Put short operating rules in
SKILL.md; put long examples, templates, catalogs, and source notes inreferences/. - Review the change. Human review matters because a bad skill update affects every future use.
- Let symlinks publish it. Since
~/.agentspoints 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.mdwhen 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
| Area | Current shape |
|---|---|
| Config source | conf/modules/starship.toml |
| Installed config | ~/.config/starship.toml |
| Shell integration | eval "$(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
| Check | Command |
|---|---|
| Binary | starship --version |
| Render config | starship explain |
ripgrep
ripgrep is installed by Home Manager and gets a small default config at
~/.config/ripgrep/config.
Defaults
| Setting | Purpose |
|---|---|
--line-number | Show file line numbers. |
--smart-case | Case-insensitive search unless the pattern has capitals. |
--max-columns=120 | Avoid unreadably long output lines. |
--max-columns-preview | Still show a preview for long lines. |
--type-add=nix:*.nix | Teach 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
| Host | User | Identity | Purpose |
|---|---|---|---|
github.com | git | keys_gh | GitHub remotes |
codeberg.org | git | keys_codeberg | Codeberg remotes |
tangled.sh | git | keys_tangled | Tangled hosted remotes |
knot.desertthunder.dev | git | keys_tangled | Knot host |
nix-baxcalibur-knot | git | keys_tangled | Tailnet Tangled Knot remotes |
The shared config sets IdentitiesOnly yes and AddKeysToAgent no.
Secret Paths
| Environment | Path 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
| Check | Command |
|---|---|
| GitHub auth | ssh -T [email protected] |
| Codeberg auth | ssh -T [email protected] |
| Tangled auth | ssh -T [email protected] |
| Knot over tailnet | ssh -T nix-baxcalibur-knot |
| Debug identity choice | ssh -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.
Quick search
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
Comment-aware search
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-configkeeps personal rg config from changing project results.--max-columns=1000avoids dropping long lines too early.- Add
-ifor case-insensitive tags. - Add
--hiddenwhen 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.