Unified Nix infrastructure for all my machines. Built on Den (aspect-oriented flake-parts framework) with import-tree for automatic module discovery.
Shell & terminal: zsh (macOS) / bash (Linux) with autocompletions, syntax highlighting, Starship prompt (Dracula theme), WezTerm, tmux with session persistence
Modern coreutils: eza, bat, fd, ripgrep, dust, duf, procs, sd, fzf, zoxide, broot, delta (side-by-side diffs)
History & snippets: atuin (fuzzy synced shell history), navi, pet, cheat
Development: Node.js, Rust (via rustup), Python (via uv), Bun, .NET, Go tooling, Neovim, Zed, Doom Emacs
Cloud & infra: kubectl, Helm, Flux, AWS CLI, Azure CLI, Pulumi, OpenShift, krew, kubie, saml2aws
Git: GPG/SSH commit signing, LFS, lazygit, delta, gh + gh-dash
Containers: Podman, Docker Compose, DevPod, Distrobox, Kind
macOS extras: Homebrew management, Touch ID for sudo (survives tmux), system defaults, AeroSpace tiling, Lix as Nix implementation
Upgrades: Run topgrade --yes to update everything in one go (Nix, Guix, Homebrew, Doom Emacs, Flatpak — then auto-GC)
| Host | Platform | Description |
|---|---|---|
| mahakala | x86_64-linux | Personal workstation — Guix System + Guix Home + Home Manager |
| M-02877 | aarch64-darwin | Work MacBook — nix-darwin + Home Manager + Homebrew |
| paphos | x86_64-linux | Home server — NixOS, Forgejo, Forgesync, Tailscale, agenix secrets (docs/paphos/) |
| pixel-fold | aarch64-linux | Android phone — nix-on-droid (proot, not NixOS) |
| oracle | aarch64-linux | Oracle Cloud Free Tier A1.Flex — OCI image, OpenTofu, Tailscale peer relay (docs/oracle/, terraform/oracle/README.md) |
- Nix (or Lix) with flakes enabled
- just (optional, for deploy shortcuts)
- Platform-specific:
darwin-rebuild(macOS),nixos-rebuild(NixOS),nix-on-droid(Android),guix(Guix System)
# Install Lix
curl -sSf -L https://install.lix.systems/lix | sh -s -- install
# Clone
git clone https://github.com/worldofgeese/den.git ~/.config/home-manager
cd ~/.config/home-manager
# First apply (bootstrap nix-darwin)
nix flake update
sudo -H nix run nix-darwin -- switch --flake .#M-02877
# Subsequent applies
just deploy-darwinAfter the first successful rebuild, open a new terminal to pick up the shell configuration.
mahakala runs Guix System as its base OS with Nix Home Manager layered on top.
# Ensure Nix is installed with flakes
# Clone to the expected path
git clone https://github.com/worldofgeese/den.git ~/.config/home-manager
cd ~/.config/home-manager
# Deploy all three layers
just deploy-mahakala# From any machine with SSH access to paphos
just deploy-paphosScaffold and operations docs: docs/oracle/. OpenTofu details: terraform/oracle/README.md.
just build-oracle-image
cp terraform/oracle/terraform.tfvars.example terraform/oracle/terraform.tfvars
# edit terraform.tfvars (private_key_path, OCIDs, bucket, image_path, ssh_public_key)
just oracle-tofu-init
just oracle-tofu-plan
just oracle-tofu-apply # explicit — not run automatically
just oracle-tofu-backup-state # gopass backup — run after apply
just deploy-oracle # after instance exists — remote nixos-rebuildMust be run on-device from the Nix-on-Droid app (not over SSH — activation requires a real terminal):
git clone https://github.com/worldofgeese/den.git ~/.config/home-manager
cd ~/.config/home-manager
nix-on-droid switch --flake .#pixel-fold# Everything on mahakala (Guix System + Guix Home + Home Manager)
just deploy-mahakala
# Only Home Manager on mahakala
just deploy-mahakala-hm
# Only Guix Home
just deploy-mahakala-guix
# Only Guix System (requires sudo)
just deploy-mahakala-system
# Work MacBook (nix-darwin)
just deploy-darwin
# Home server (remote NixOS)
just deploy-paphos
# Oracle Cloud NixOS (remote aarch64)
just deploy-oracle
# Android phone (on-device only)
just deploy-pixel-foldManual fallback (run only if just is unavailable):
home-manager switch --flake .#worldofgeese # mahakala HM
darwin-rebuild switch --flake .#M-02877 # macOS
nixos-rebuild switch --flake .#paphos --target-host paphos --use-remote-sudo
nix-on-droid switch --flake .#pixel-fold # Android (on-device)
sudo guix system reconfigure guix/system.scm # Guix System
guix home reconfigure guix/home-configuration.scm # Guix HomeThere are also shell aliases on M-02877:
fleek-apply # hostname-agnostic darwin-rebuild
apply-M-02877 # same thing, explicittopgrade --yesOn mahakala, topgrade delegates the Nix and Guix work to the Justfile rather than its own built-in modules: its nix, home_manager, guix, system, and several other modules are disabled in workstation.nix, and pre-commands run just upgrade-kernel (CachyOS kernel metadata) then just deploy-mahakala (Guix System + Guix Home + Home Manager, including flake input and pinned Rust tool updates). topgrade itself then handles Doom Emacs, Distrobox, Homebrew-in-distrobox, and post-run garbage collection across Nix, Guix, Flatpak, and Podman. On M-02877 it runs just update-darwin (every flake input except the Linux-only ones listed in darwin-skip-inputs) and then just deploy-darwin; its built-in VS Code, VS Code Insiders, Cursor and Microsoft Office steps are disabled because those apps update themselves, and its macOS system step is disabled because installing OS updates needs an admin password and Jamf/Nudge manage them.
cd ~/.config/home-manager && just update
just deploy-darwin # or deploy-mahakala-hm, etc.just update-input nixpkgs# Update den to latest
nix flake update den
# Verify evaluation passes
nix eval .#darwinConfigurations.M-02877.config.home-manager.users --apply builtins.attrNames
# Build and switch
darwin-rebuild build --flake .#M-02877
sudo darwin-rebuild switch --flake .#M-02877If eval fails after an update, check den's changelog and docs for migration notes:
Key things to know about our den usage:
den.batteries.define-userauto-setshome.usernameandhome.homeDirectorybased on host classden.schema.user.classes = [ "homeManager" ]enables automatic HM forwarding for all users- Never manually import
darwinModules.home-manageror wirehome-manager.users.<name>— den handles forwarding fromden.aspects.<user>.homeManagerautomatically
Lix comes from pkgs.lixPackageSets.latest.lix. Bump nixpkgs to get a newer Lix:
just update-input nixpkgs
just deploy-darwin
nix --version # reflects the new LixDon't run nix upgrade-nix — it doesn't know about Lix's release channel.
Every .nix file under modules/ is automatically imported as a top-level flake-parts module (via import-tree). Files prefixed with _ are skipped.
den.hosts.<system>.<hostname>— Declares a full system (NixOS or nix-darwin) with usersden.homes.<system>.<username>— Declares standalone Home Manager configs (no system management)den.aspects.<name>— Reusable feature bundles with.nixos,.darwin, and.homeManagersub-attributesden.default— Global settings applied to all hosts/homes
Exception: pixel-fold bypasses the Den framework entirely because nix-on-droid requires its own nixOnDroidConfiguration builder that is incompatible with flake-parts host wiring. It uses a raw flake.nixOnDroidConfigurations output and does not receive global Den defaults or aspects.
modules/
├── dendritic.nix # Imports Den framework
├── hosts.nix # Host/home declarations
├── defaults.nix # Global defaults + aspect wiring
├── worldofgeese.nix # Shared user aspect (git, shell tools, direnv, atuin)
├── git-common.nix # Shared git settings aspect (hosts add identity)
├── terminal.nix # Shared terminal UX (Dracula delta, previews, persistence)
├── shared-devtools.nix # Dev tooling shared by mahakala + M-02877
├── workstation.nix # Desktop/dev packages + topgrade
├── ssh.nix # Fleet SSH matchBlocks (Tailscale hostnames)
├── server.nix # Reusable NixOS server aspect
├── overlays.nix # Nixpkgs overlays (devenv, decapod)
├── mahakala.nix # Wires worldofgeese → workstation + ssh (mahakala-specific)
├── M-02877/
│ ├── darwin.nix # macOS system (Lix, Homebrew, Touch ID, zsh, system defaults)
│ └── dktaohan.nix # Work user (separate git identity, full dev toolchain)
├── paphos/
│ ├── system.nix # Auto-upgrade, users, sudo, boot
│ ├── hardware.nix # Disks, LUKS, Dropbear initrd SSH
│ ├── networking.nix # Tailscale, openssh, locale
│ └── forgejo.nix # Forgejo + Forgesync + agenix secrets
├── oracle/
│ ├── _configuration.nix # OCI guest: ssh, cloud-init, firewall 22 (import-tree skipped)
│ └── image.nix # packages.aarch64-linux.oracle-image + nixosConfigurations.oracle
└── pixel-fold/
├── system.nix # nix-on-droid config (custom flake output, pinned inputs)
└── _home.nix # Home Manager config (skipped by import-tree, loaded by system.nix)
Secrets are managed with agenix. Encrypted .age files live in secrets/ and are decrypted at activation time on the target host.
To edit a secret:
nix run github:ryantm/agenix -- -e secrets/forgejo-runner-token.ageKeys authorized to decrypt are declared in secrets/secrets.nix.
secretspec holds per-user API secrets, which are declared in secretspec.toml. Every secret goes through the personal provider alias. Home Manager (modules/shared-devtools.nix) generates ~/.config/secretspec/config.toml on each host to define it:
- M-02877:
personalissecretspec.age, an age-encrypted file committed next tosecretspec.toml. It is encrypted to the two post-quantum public keys insecretspec.age.recipients:- this Mac's Secure Enclave key (
age-plugin-se, Homebrew). It decrypts with no dialog and never touches the Keychain, so secretspec rebuilds no longer prompt (cachix/secretspec#438). - a backup key that lives only in your password manager.
- this Mac's Secure Enclave key (
- Linux hosts:
personalis the system keyring.
The Secure Enclave key can't leave the machine, and it has no access control, so any process running as you can decrypt while on this Mac. The backup key is the only way to read the file anywhere else. Keep it in a password manager.
On a new or rebuilt Mac, deploy, then re-key the file to that Mac's Secure Enclave:
pbpaste | just secretspec-se-setup # after copying the backup key from the password manager
git add secretspec.age secretspec.age.recipients && git commitAfter just secretspec-se-setup, secretspec set NAME writes to both recipients as usual.
The guix/ directory contains GNU Guix configurations for the mahakala workstation (system + home). These are managed independently from Nix. Use the Justfile recipes:
just deploy-mahakala-guix # Guix Home only
just deploy-mahakala-system # Guix System only (requires sudo)Manual fallback (run only if just is unavailable):
sudo guix system reconfigure guix/system.scm
guix home reconfigure guix/home-configuration.scmGuix manages: kernel, desktop environment (GNOME), system services (PipeWire, GPG agent, dbus), fonts, and desktop packages (Emacs, Podman, Steam, browsers).
Nix Home Manager manages: shell development tools, overlays, direnv, starship, atuin, and dev-oriented CLI packages.
The two coexist via ~/.nix-profile being sourced in the Guix shell profile.
The pixel-fold configuration uses pinned versions of nixpkgs and home-manager (nixpkgs-nod and home-manager-nod inputs) rather than following the main nixpkgs-unstable. This works around a proot pty bug where glibc 2.42's TCGETS2 ioctl causes "getting pseudoterminal attributes: Permission denied" during activation.
The pins match nix-on-droid release-24.05's tested versions. Remove them once PR #529 (proot-termux update) is merged and you can reinstall from a new bootstrap zip.
Important: nix-on-droid switch must be run on-device (from the Nix-on-Droid app), not over SSH — the activation step requires a proper terminal.
Nix flakes require all referenced files to be tracked by git:
git add -A && git commit -m "track new files"The Lix installer's /etc/nix/nix.conf conflicts with nix-darwin. Move it aside:
sudo mv /etc/nix/nix.conf /etc/nix/nix.conf.before-nix-darwin
just deploy-darwinHappens during bootstrap before nix-darwin writes its own nix.conf. Pass the flag inline:
sudo -H nix --extra-experimental-features "nix-command flakes" run nix-darwin -- switch --flake .After the first successful rebuild this goes away.
nix flake updateThis happens when Home Manager's programs.ssh creates symlinks in ~/.ssh/ that Guix's activation script then tries to chmod. The Guix config already skips symlinks — if you see this, ensure guix/home-configuration.scm has the (unless (symbolic-link? file) ...) guard in the ssh-permissions service.
This is the proot pty bug. Ensure you're using the pinned inputs (nixpkgs-nod / home-manager-nod). If you've accidentally updated the lock, restore the pins:
nix flake update nixpkgs-nod home-manager-nod nix-on-droidIf you see this after a Nix-on-Droid app update, you may need to reinstall from a fresh bootstrap zip (wipes app data).
nix --extra-experimental-features "nix-command flakes" run nix-darwin#darwin-uninstaller
/nix/lix-installer uninstallIf you get "volume in use":
sudo diskutil unmountDisk force /nix && sudo diskutil apfs deleteVolume "Nix Store"home-manager uninstall
/nix/nix-installer uninstall # or /nix/lix-installer uninstallUninstall the Nix-on-Droid app from Android settings.