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
Summary
What I use
The working set, not a full package inventory. The program pages cover the settings and failure modes I think are worth remembering.
| Area | Current choice |
|---|---|
| System | NixOS or Fedora, flakes, Home Manager, and host-specific modules |
| Desktop | GNOME, or Umbriel with Noctalia |
| Terminal | Ghostty, usually running Zellij |
| Shell | Zsh with Starship |
| Editor | Neovim and Zed in vim mode |
| Notes | Neovim and 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: flakes, modules, and the language itself
- NixOS: hosts, rebuilds, and SOPS
- Programs: per-application config and dotfiles
- Other distros: how to recreate the setup without NixOS
- Tools: small command-line notes and scripts
Where to start
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.
These docs are working notes, not marketing copy. Use direct sentences, and name the command, file, or setting.
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 whether you would actually read it. Long documentation goes unread.
- Do not summarize a section you just explained.
- Keep one point per paragraph.
Style
- Use commands and paths when they answer the question faster than prose.
- Drop vague claims such as “important”, “powerful”, or “useful” unless the sentence says why.
- Cut filler openings: “it’s worth noting”, “the goal is”, “this serves as”, “despite these challenges”.
- Replace passive voice when the actor matters.
Nix concepts
Background on the Nix ideas this repo depends on. Operational pages link here instead of re-explaining flakes, modules, or Home Manager.
| 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. |
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, including Zed and AI agent CLIs. |
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
A checklist for debugging system and user 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 display-manager.service
systemctl --user status noctalia.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 noctalia.service --no-pager
Search messages for a word or regex:
journalctl --user -b -g 'noctalia|umbriel|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 with Umbriel and Noctalia. |
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/noct.nixconf/modules/de/umb.nixconf/services/searxng.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 and UPower enabled
- Umbriel compositor and Noctalia shell enabled through a host-specific module
- SearXNG listening on
127.0.0.1:9090
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; Umbriel, Noctalia, and SearXNG stay 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 |
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 references /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. |
Adding a new machine
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 Umbriel and Noctalia.
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
Each service page covers ownership, 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 |
|---|---|
| SearXNG | Local metasearch on Haxorus. |
| 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. |
SearXNG
Haxorus runs a private SearXNG instance at http://127.0.0.1:9090. The firewall stays closed and the service listens only on loopback, so other machines cannot connect to it.
Search API
The instance enables the browser interface, JSON output, and RSS output. RSS is
the XML format supported by SearXNG; SearXNG does not provide a separate
format=xml response.
curl 'http://127.0.0.1:9090/search?q=nixos&format=json'
curl 'http://127.0.0.1:9090/search?q=nixos&format=rss'
The preferences page is unlocked so you can change the theme, language, categories, safe search, autocomplete, and other exposed search settings.
State and configuration
The NixOS module is conf/services/searxng.nix, and Haxorus enables it from
conf/machines/thinkpad/configuration.nix.
The service generates its secret once at /var/lib/searx/environment and
reuses it across rebuilds. The file is readable only by root and the searx
service account. No secret is stored in the Nix store.
Checks
systemctl status searx
curl --fail http://127.0.0.1:9090/
journalctl -u searx -b
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.
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 |
Access paths
| 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.
Ingress and SSH
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.
State and media
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 mostly matter during first scans, cover generation, and imports.
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 staged files into the library roots:
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
Read from the tailnet URL on desktop, Android, and tablet clients. The web UI is the supported client; OPDS and third-party clients can be added later.
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 Umbriel with Noctalia, PipeWire, NetworkManager, and 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 or Umbriel, Noctalia, portals, and a login manager. |
| 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 |
| OpenCode | Nix or upstream package |
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. Aim for the same working environment rather than 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.
Reusable agent instructions and skills live under conf/agent. Codex and Pi
settings live in their native global directories because they contain
machine-specific paths and application state. See Agent skills
for the split and the reconstruction notes.
Programs
Each page covers intent, ownership, key settings, and troubleshooting. They do not mirror whole config files or repeat package lists already visible in the Nix source.
- Ghostty
- Fastfetch
- Noctalia
- Umbriel
- tmux
- Zellij
- Zed
- Zathura
- Neovim
- Git
- Zsh
- Starship
- ripgrep
- SSH
- Obsidian
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 |
|---|---|
GNOME Ctrl-Alt-t | ghostty |
GNOME Super-z | ghostty -e zellij |
Umbriel Super-Return | ghostty |
Umbriel 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 |
Noctalia
Haxorus uses Noctalia for its Wayland shell. It replaces Waybar, rofi, cliphist, mako, SwayOSD, hyprpaper, hypridle, hyprlock, the power menu, and the old screenshot scripts. Umbriel provides the compositor.
Session
Choose Umbriel in GDM. Umbriel starts umbriel-session.target, which starts
Noctalia as a systemd user service. Noctalia then provides the shell surfaces
and desktop services for that session.
Noctalia is configured in conf/modules/de/noct.nix. The module imports its
NixOS and Home Manager modules. The ThinkPad configuration imports noct.nix.
The first rebuild must accept the Noctalia binary cache configuration:
sudo nixos-rebuild switch --accept-flake-config \
--flake "$NIXOS_CONFIG#nix-haxorus"
That generation adds the cache to the system Nix settings, so later rebuilds
can use the normal rebuild alias.
Configuration ownership
Home Manager generates the read-only Noctalia configuration at
~/.config/noctalia/config.toml from programs.noctalia.settings in
noct.nix.
Noctalia stores changes made through Settings and the desktop widget editor in
~/.local/state/noctalia/settings.toml. This writable file loads after the
Home Manager file, so a saved GUI value overrides the corresponding value in
noct.nix.
Delete the relevant key from settings.toml, or delete the whole file while
Noctalia is stopped, to return that setting to Home Manager. Use
Settings → Export Config → Merged User Config to inspect or promote GUI
changes. Move wanted values into noct.nix, rebuild, and remove their GUI
overrides. Do not edit the Home Manager symlink under ~/.config.
Shell surfaces
The floating top bar has an 8 px screen margin and rounded corners. It contains:
- the launcher and per-output workspaces on the left;
- the active media player in the center;
- network, Bluetooth, volume, brightness, battery, idle inhibition, notifications, tray, clock, Control Center, and session controls on the right.
The launcher searches desktop applications and includes calculator, emoji,
session, wallpaper, and window providers. Prefix a search with /calc, /emo,
/session, /wall, or /win to select one provider.
Control Center provides media and stream controls, display brightness, system status, NetworkManager, Bluetooth, notification history, and UPower battery information. Weather, calendar, and screen-time tabs are hidden because those services are not configured. Session actions are also available in the separate session panel.
Noctalia is the notification daemon. Dismissing a toast keeps it in Control Center history. Do Not Disturb suppresses new toasts without stopping history.
The dock and desktop widgets are disabled.
The lock screen uses wall00.png, a dark tint, password authentication, and
fingerprint authentication. Noctalia locks before suspend, including suspend
caused by closing the lid.
Theme
Noctalia uses Inter and the built-in dark Eldritch palette. The custom
Haxorus palette remains commented out in noct.nix so it can be restored
without reconstructing its color roles.
Panels are solid and bordered rather than glassy. Application theme templates are disabled, so the palette does not rewrite GTK, Qt, terminal, or editor themes.
Idle and power
Noctalia is the only idle daemon:
| Idle time | Action |
|---|---|
| 5 minutes | Lock the session. |
| 5:30 | Turn displays off. Activity turns them on again. |
| 15 minutes | Lock and suspend the machine. |
Wayland idle inhibitors are honored. The bar’s caffeine control can also keep the session awake.
UPower supplies battery status and health. TLP controls CPU and laptop power
policy. power-profiles-daemon is disabled to avoid conflicting with TLP, so
Noctalia can show battery information but has no power-profile selector.
Screenshots and media keys
The Umbriel keymap sends screenshot and media actions to Noctalia. Screenshots
open its annotation editor, save under ~/Pictures/Screenshots, and copy the
result to the clipboard. Media keys control volume, microphone, brightness, and
playback; Noctalia displays the corresponding OSD.
See Umbriel for the complete keymap.
Debugging
Check the service, log, IPC connection, and merged configuration:
systemctl --user status noctalia.service
journalctl --user -b -u noctalia.service
noctalia status
noctalia config validate
noctalia config export full | yq -p toml '.'
If a declared setting is ignored, inspect the winning GUI layer:
$EDITOR ~/.local/state/noctalia/settings.toml
Noctalia requires an Umbriel session for layer-shell, session lock, workspace, and output integration. See Umbriel debugging when the service runs but compositor integration fails.
Umbriel
Haxorus uses Umbriel as its Wayland compositor. It owns outputs, workspaces, window placement, input, and compositor effects. Noctalia provides the shell and desktop services.
Choose Umbriel in GDM to start the session. X11 applications run through
xwayland-satellite. Screen sharing and portal screenshots use
xdg-desktop-portal-umbriel.
Configuration
conf/modules/de/umb.nix enables the Umbriel NixOS module and configures
programs.umbriel.settings through Home Manager. The ThinkPad configuration
imports umb.nix.
Home Manager generates ~/.config/umbriel/config.toml from the Nix attribute
set. Umbriel does not write overrides or modify that file. Change umb.nix and
rebuild instead of editing the Home Manager symlink. Umbriel watches the file
and applies valid updates without a restart; it keeps the last working config
if a reload fails.
The repository sets the session environment, laptop output, workspace model,
layout, colors, appearance, input, keybindings, window rules, and animations.
An autostart command publishes the Wayland environment and retries
umbriel-session.target until systemd accepts it. This prevents a user-manager
startup race from leaving the session without Noctalia. Unspecified settings
retain Umbriel’s defaults. See the upstream
configuration reference
when adding an option.
Window-management model
Every workspace uses Dwindle. There are no Master, Scrolling, or per-workspace
layout overrides. New windows split the focused tile along its longer edge and
open in the right or bottom half. preserve_split = true keeps that split
direction when surrounding geometry changes.
Tiled windows have 4 px gaps. Windows use 8 px corners and a 1 px border, with
no outer border, blur, or shadow. Unfocused windows use 97% opacity. Colors come
from desktop-theme.nix.
Super-Shift-F toggles the focused window between tiled and floating, then
centers it. Other floating windows retain their own size and position unless a
window rule supplies defaults. Super plus left or right mouse drag moves or
resizes a window.
Workspaces
The laptop output has ten static, numbered positions. Empty workspaces remain
available. Super-1 through Super-0 select positions 1 through 10 on the
output under the pointer. Re-selecting the active workspace returns to the
previous workspace on that output.
Workspaces belong to outputs rather than forming one global list. An unconfigured external output uses Umbriel’s default dynamic workspace model. It starts with one workspace, adds an empty workspace after the last occupied one, and removes other empty inactive workspaces.
No workspace currently overrides the global Dwindle layout. Add a workspace
rule in umb.nix if a workspace needs Master or Scrolling.
When an output disconnects, Umbriel moves its windows to another enabled output. If the output reconnects, its workspaces, windows, active workspace, and layout state return to it.
Outputs
The ThinkPad panel is eDP-1. It starts at [0, 0], uses scale 1.2, and
arranges its workspaces horizontally. Its mode is not fixed, so Umbriel uses
the display’s preferred mode.
List connected outputs and their copyable configuration names from an Umbriel session:
umbriel outputs
umbriel outputs --json
Add or change an output.<name> entry in umb.nix to set a monitor’s mode,
scale, position, workspace inventory, or workspace axis. Prefer the reported
monitor name for rules tied to one physical display. Prefer a connector such as
DP-1 for rules tied to one port.
Outputs without an explicit position are placed automatically from left to right. Docking and undocking therefore does not require a fixed arrangement. Disconnected windows return to their original output when it reconnects.
Scratchpad
There are no named scratchpads. Umbriel therefore provides its implicit global
default scratchpad:
Super-Shift-Sstores the focused window.Super-Sshows or hides all stored windows on the pointer output.umbriel msg window-restore-from-scratchpadrestores the focused stored window to its saved output and workspace.
There is no restore keybinding. Showing a scratchpad does not remove its windows from it. Scratchpad windows float, and the visible scratchpad dims the output by 30%. The scratchpad can roam between outputs and returns with an output after a disconnect and reconnect.
Keybindings
Mod is fixed to Super, including nested sessions. Bindings dispatch
Umbriel actions or spawn commands.
The same action strings can be run with umbriel msg <action>.
Applications
| Key | Action |
|---|---|
Super-Return | Open Ghostty. |
Super-Z | Open Ghostty running Zellij. |
Super-B | Open Zen Browser. |
Super-E | Open Nautilus. |
Windows
| Key | Action |
|---|---|
Super-Q | Close the focused window. |
Super-Shift-F | Toggle floating and center the window. |
Super-Shift-G | Toggle fullscreen. |
Super-Shift-H | Toggle maximize. |
Super-Shift-P | Toggle pinned state. |
Super-Ctrl-H/L | Decrease or increase width by 10%. |
Super-Ctrl-K/J | Decrease or increase height by 10%. |
Super-Alt-H/L | Move the Dwindle column left or right. |
Super-Alt-K/J | Move the window up or down. |
Super-Mouse left drag | Move a tiled or floating window. |
Super-Mouse right drag | Resize a tiled or floating window. |
Focus
| Key | Action |
|---|---|
Super-H, Super-Left | Focus left. |
Super-J, Super-Down | Focus down. |
Super-K, Super-Up | Focus up. |
Super-L, Super-Right | Focus right. |
Focus follows the pointer. Moving the pointer does not warp it to the focused window, and typing does not hide it.
Workspaces
| Key | Action |
|---|---|
Super-1..0 | Switch to workspace 1 through 10. |
Super-Shift-1..0 | Move the focused window to workspace 1–10. |
Super-Wheel up | Switch to the previous workspace. |
Super-Wheel down | Switch to the next workspace. |
Wheel switching has a 150 ms cooldown. Three-finger horizontal touchpad swipes also switch workspaces.
Scratchpad
| Key | Action |
|---|---|
Super-S | Show or hide the default scratchpad. |
Super-Shift-S | Move the focused window to the scratchpad. |
Noctalia
| Key | Action |
|---|---|
Super-R, Super-Space, Super-P | Toggle the launcher. |
Super-V | Toggle clipboard history. |
Super-Shift-V | Clear clipboard history. |
Super-N | Invoke the latest notification. |
Super-Shift-N | Toggle Do Not Disturb. |
Media
| Key | Action |
|---|---|
Volume up/down | Change output volume. |
Volume mute | Toggle output mute. |
Microphone mute | Toggle microphone mute. |
Brightness up/down | Change display brightness 5%. |
Media next/previous | Change tracks. |
Media play/pause | Toggle playback. |
These hardware-key bindings work while the session is locked.
Screenshots
| Key | Action |
|---|---|
Print | Capture a region with Noctalia. |
Shift-Print | Capture the focused output with Noctalia. |
Screenshot bindings do not repeat.
Session
| Key | Action |
|---|---|
Super-Shift-L | Lock through Noctalia. |
Super-Escape | Toggle Noctalia’s session panel. |
Super-Shift-/ | Toggle Umbriel’s keybinding cheatsheet. |
Super-Shift-R | Reload the Umbriel configuration. |
Power controls are available from the Noctalia session panel.
Window rules
The rules in umb.nix provide these exceptions:
- Unfocused windows use 97% opacity.
- Noctalia Settings opens floating at 1020×900.
- Umbriel’s share picker opens floating at 800×600.
- Calculator, Nautilus, PulseAudio controls, NetworkManager’s connection editor, GNOME Settings, and desktop portal windows open floating.
- Browser picture-in-picture windows open floating, 20 px from the bottom-right corner.
Use umbriel windows to inspect the app ID and title before adding a rule. Keep
rules for application behavior in umb.nix rather than relying on a window’s
current title or position by hand.
Debugging
Validate the generated config and inspect compositor state:
umbriel validate
umbriel outputs
umbriel workspaces
umbriel windows
umbriel layers
Inspect the session services and logs when startup or integration fails:
systemctl --user status umbriel.service noctalia.service
journalctl --user -b -u umbriel.service -u noctalia.service
journalctl --user -b -u xdg-desktop-portal-umbriel
Umbriel can run nested in GNOME for basic compositor testing, but output,
session lock, input gesture, and portal behavior must be tested in a real
Umbriel session. Run umbriel validate after input updates, then test
lock/unlock, suspend, screenshots, external outputs, and screen sharing.
tmux
tmux is installed for owais on every Home Manager host.
Keys
tmux keeps its standard Ctrl-b prefix. Copy mode and command prompts use vi
bindings.
| Key | Action |
|---|---|
Ctrl-b h/j/k/l | Move between panes |
Ctrl-b H/J/K/L | Resize the active pane |
Ctrl-b | | Split left and right |
Ctrl-b - | Split top and bottom |
Ctrl-b c | Create a window in the current path |
Ctrl-b [ | Enter copy mode |
v, V, Ctrl-v | Select characters, lines, or a block |
y, Enter | Copy to the Wayland clipboard |
h/j/k/l, w/b, 0/$ | Move in copy mode |
/, ?, n, N | Search in copy mode |
Ctrl-b d | Detach and leave the session running |
Splits and new windows inherit the active pane’s working directory. Mouse selection and pane controls are enabled, and each pane keeps 100,000 lines of history.
Theme
The status line adapts the Zellij marble theme:
| Element | Color |
|---|---|
| Background | #151516 |
| Surface | #181818 |
| Text | #cfcfcf |
| Muted text | #7a7a7a |
| Border | #2a2a2a |
| Active accent | #51a4e7 |
Home Manager writes the generated configuration to
~/.config/tmux/tmux.conf. Existing tmux servers keep their loaded settings;
run tmux source-file ~/.config/tmux/tmux.conf to reload one.
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 git@github.com |
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.
Pi
Home Manager installs Pi from unstable Nixpkgs. Pi packages remain local because
Pi manages their files and records them in ~/.pi/agent/settings.json.
Install the MCP adapter with Pi’s package manager:
pi install npm:pi-mcp-adapter
Restart Pi after installation. If an MCP config already exists at .mcp.json
or ~/.config/mcp/mcp.json, the adapter loads it automatically. Otherwise,
start Pi and run:
/mcp setup
Use .mcp.json for project servers and ~/.config/mcp/mcp.json for servers
that should be available in all projects. Check the installation with:
pi list
Pi packages can execute code with the user’s permissions. Review the adapter’s source before installing or updating it.
Codex
Log in through the TUI.
Claude Code
Home Manager installs the standard Claude Code package from unstable Nixpkgs.
No provider or authentication settings are managed by this repo. Run claude
and follow its login flow.
Four files are linked into ~/.claude/:
| Link | Source | Purpose |
|---|---|---|
CLAUDE.md | conf/agent/AGENTS.md | Global instructions. |
skills/ | conf/agent/skills/ | Repository skills. |
settings.json | conf/agent/claude-settings.json | Status line, effort, theme. |
statusline.sh | conf/agent/claude-statusline.sh | Status line renderer. |
settings.json sets effortLevel to high for claude-opus-5, the theme to
dark-ansi, and points statusLine at $HOME/.claude/statusline.sh. The
script reads Claude Code’s JSON status payload on stdin and prints the working
directory, git branch, model name, and the remaining share of the context
window and of the five-hour and seven-day rate limits.
Claude Code writes to settings.json itself, so the link points out of the Nix
store at the repository file. Changing the theme with /config edits
conf/agent/claude-settings.json and shows up in git status. Commit the
change or revert it. If Claude Code ever replaces the symlink with a regular
file, copy that file back over conf/agent/claude-settings.json and rebuild;
otherwise the next rebuild fails on the conflict, because
home-manager.backupFileExtension is null.
OpenCode
Home Manager installs OpenCode from unstable Nixpkgs. Run opencode and follow
its provider setup flow.
Reviewing agent changes
Home Manager installs hunk from unstable Nixpkgs on every machine. It is a terminal diff viewer for agent-authored changesets and reads Git, Jujutsu, and Sapling repositories. Every review starts from a subcommand:
hunk diff
hunk diff --staged
hunk diff --watch
hunk show
hunk diff --watch reloads as the agent edits files. Nothing about hunk is
configured here; run hunk --help for layout, theme, and extension options.
Ownership
conf/shared.nix:pkgsUnstable.claude-codeconf/shared.nix:pkgsUnstable.opencodeconf/shared.nix:pkgsUnstable.hunkconf/shared.nix: thehome.filelinks into~/.claude/conf/agent/:AGENTS.md,skills/,claude-settings.json,claude-statusline.sh
Validate
After rebuilding, open a fresh shell and run:
claude doctor
opencode --version
hunk --version
Check that the Claude Code links resolve into the repository:
readlink -f ~/.claude/CLAUDE.md ~/.claude/skills ~/.claude/settings.json
Agent skills
This repository maintains four reusable skills under conf/agent/skills/:
| Skill | Use it for |
|---|---|
css | Vanilla CSS structure, component classes, tokens, accessible colours, and replacing utility CSS. |
frontend-design | Building, redesigning, reviewing, and polishing accessible, responsive web interfaces. |
svelte-testing | Svelte and SvelteKit unit, component, server, SSR, browser, and end-to-end testing. |
writing | Drafting and revising prose in a direct human voice. |
Each skill has a SKILL.md file and can include supporting files:
conf/agent/skills/<skill>/
├── SKILL.md
├── references/
├── scripts/
└── assets/
Only SKILL.md is required. Keep detailed references, examples, templates, and
helper programs beside it so the skill remains self-contained.
How skills load
An agent scans its configured skill directories at startup and puts each
skill’s name and description into the model context. The model reads the full
SKILL.md only when the request matches that description. This keeps inactive
skill instructions out of the context.
Pi scans both ~/.agents/skills/ and ~/.pi/agent/skills/ for global skills. It
also scans .agents/skills/ and .pi/skills/ in trusted projects. Skills can
also come from installed Pi packages, the skills setting, or repeated
--skill <path> arguments.
Use /skill:<name> in Pi to load a skill explicitly. For example:
/skill:writing revise docs/src/introduction.md
Skill format
SKILL.md starts with YAML frontmatter:
---
name: css
description: Write, refactor, and review well-structured vanilla CSS...
---
The name must contain only lowercase letters, numbers, and hyphens. The
description should name the work and the situations that should trigger the
skill. Relative links in SKILL.md resolve from the skill directory.
Pi follows the Agent Skills specification and reports malformed frontmatter, invalid names, missing descriptions, and name collisions when it scans skills.
Publishing repository skills
On NixOS, Home Manager publishes the complete repository skill directory at
~/.agents/skills:
home.file.".agents/skills" = {
source = config.lib.file.mkOutOfStoreSymlink "${agentConfigDir}/skills";
force = true;
};
This configuration is defined in conf/shared.nix. Changes under
conf/agent/skills/ become available through the symlink without copying the
files.
On machines not managed by this Home Manager configuration, link each desired
skill into ~/.agents/skills/. Linking skills individually allows repository
skills and machine-installed skills to share the directory:
mkdir -p ~/.agents/skills
ln -s "$PWD/conf/agent/skills/writing" ~/.agents/skills/writing
Pi and Codex both discover skills from ~/.agents/skills/, so a second copy in
an agent-specific directory is usually unnecessary. Review third-party skills
before installing them because their instructions and scripts run with the
agent’s permissions.
Claude Code does not read ~/.agents/skills/. It scans ~/.claude/skills/, so
Home Manager publishes the same directory a second time:
home.file.".claude/skills" = {
source = config.lib.file.mkOutOfStoreSymlink "${agentConfigDir}/skills";
force = true;
};
Both links point at conf/agent/skills/, so there is still one copy to edit.
The frontmatter this repository already uses satisfies Claude Code, which
requires name and description and nothing else.
The script conf/agent/link-global-instructions.sh installs the same links on
machines without Home Manager. It links AGENTS.md for Codex, Pi, and Claude
Code, and links the skill directory for Claude Code.
Updating a skill
Put short operating instructions in SKILL.md. Put long source notes,
templates, catalogs, and examples in references/. Keep project-specific facts
in the owning project’s documentation rather than in a reusable skill.
After changing a skill, start a new agent session so the harness scans its
frontmatter again. In Pi, /skill:<name> can then confirm that the expected
skill loads.
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 prompt shows the directory, Git state, language and runtime context, command duration, background jobs, exit status, and the 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 git@github.com |
| Codeberg auth | ssh -T git@codeberg.org |
| Tangled auth | ssh -T git@tangled.sh |
| Knot over tailnet | ssh -T nix-baxcalibur-knot |
| Debug identity choice | ssh -vT git@github.com |
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 same configuration enables Obsidian’s command-line interface. It applies to
both nix-haxorus and nix-baxcalibur after rebuild.
The Nix package provides both obsidian and its obsidian-cli helper. Do not
use Register CLI in Obsidian’s settings. The desktop app runs through the
Nixpkgs Electron wrapper, so Obsidian mistakes the shared electron executable
for its launcher. Home Manager already puts the correct commands on PATH.
The desktop app must be running before most CLI commands can access a vault. Verify the CLI with:
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.
Inspect Git history before reading code
Git history can help you choose which parts of an unfamiliar repository to read first. Use it to find files that change often, areas associated with bug fixes, knowledge concentrated among a few contributors, and signs of repeated release failures.
These signals guide investigation. They do not prove that code is defective. Exclude generated files, lockfiles, vendored code, dependency updates, and broad formatting commits where possible.
Churn hotspots
List the files changed most often during the past year:
git log --format=format: --name-only --since="1 year ago" \
| sort \
| uniq -c \
| sort -nr \
| head -20
Run this against an application source directory when the repository root contains substantial generated or administrative files. A frequently changed file may be under healthy active development. Give it more attention when it also appears in bug-fix commits, lacks tests, or has unclear ownership.
Contributor concentration
Compare all-time authorship with recent authorship:
git shortlog -sn --no-merges
git shortlog -sn --no-merges --since="6 months ago"
A large concentration of commits under one author can indicate that important knowledge depends on one person. A historically prominent author who is absent from recent work may indicate a knowledge-transfer risk.
Contributor counts need context. Squash merges and repository migrations can attribute work to the merger or omit earlier history.
Bug clusters
Find files touched by commits whose messages mention common bug-fix terms:
git log -i -E --grep="fix|bug|broken" --name-only --format='' \
| sort \
| uniq -c \
| sort -nr \
| head -20
Compare these results with the churn list. Files near the top of both lists are good candidates for closer inspection. This search depends on descriptive commit messages and can miss fixes recorded under vague or project-specific terms.
Commit activity
Count commits by month:
git log --format='%ad' --date=format:'%Y-%m' \
| sort \
| uniq -c
Changes in monthly volume can reflect development pauses, release batching, holidays, repository splits, or process changes. Treat the result as team and project history rather than a code-quality score.
Reverts and emergency fixes
Search recent commit subjects for release failures and emergency work:
git log --oneline --since="1 year ago" \
| grep -iE 'revert|hotfix|emergency|rollback'
Frequent results may point to weak tests, missing staging coverage, or a risky deployment process. No matches may indicate stable releases or merely different commit-message vocabulary.
Choosing what to read first
Prioritize files or areas where several signals overlap:
- high churn and repeated bug-fix commits;
- core code with few tests;
- ownership concentrated under an inactive contributor;
- repeated rollback, hotfix, or revert history;
- recent changes in authentication, billing, permissions, migrations, or data deletion.
Use the results to form questions and select files. Confirm each suspected risk by reading the code, tests, issue history, and deployment documentation.
Adapted from Ally Piechowski, The Git Commands I Run Before Reading Any Code.
Resources
Programming Fonts is a good way to try out new fonts.