My dotfiles.
These are managed using Nix.
systemis used for machine-level Nix configuration (OS services, system packages, platform settings).homeis used for user-level Home Manager configuration (shell, editor, per-user tools).direnvis used for directory-specific development shells. For example, whencd-ing into~/code/myproject, adirenvshell can be loaded with agotoolchain that is otherwise not on thePATH.
The flake is composed of hosts and features. A feature covers one concern, such as the shell or the desktop: it contributes configuration for that concern to each module system that builds a host (NixOS, nix-darwin, system-manager and Home Manager), and it can include other features. A host lists its top-level features, and the resolver follows their includes.
For a fuller walk through host → features → OS adapter → outputs, see
docs/architecture.md.
Every feature lives in one directory under features/ and registers itself
under flake.features.<name>. The directories are discovered automatically, and
only a <name>/default.nix one level down is loaded.
A feature is top-level when a host composes it in its own right. Everything else
is a child of its parent feature, registered under that feature's provides and
named <parent>.<child>, such as base.zsh or desktop.gnome. A child applies
only when something lists it in includes, so a parent lists the children that
always apply and puts the rest under the OS scopes. Anything can list a single
child on its own: hosts/bonington takes
features.work.provides.claude-managed-settings without the rest of work.
The twenty-one top-level features:
base: Core cross-platform CLI tooling and shell/editor configuration. Children:zsh,neovim,gh,ssh,starship,cli-tools,catppuccin,motd,scripts,nix,sudo, and per OSopenssh,restic,system-manager-shell,homebrewandmacos-defaults.desktop: GUI and desktop tooling. Children: the terminals, the editors,chrome,fonts,gpg-agent, and per OSgnome,usbguard,plymouth,console,tailscale,wineand the rest.development: Personal development project shells and language toolchains. Children:debuginfod,orbstack.work: Work-specific project shells, identity defaults, and tooling. Children:falcon,kolide,claude-managed-settings.home: Personal identity. On hosts withaiit adds TeamClaude and the Cloudflare MCP servers, and on non-NixOS Linux it adds thedebianchild with the Debian, Ubuntu and GNOME project directories.ai: The shared MCP servers, skills and instructions, with one child per harness (claude-code,codex,pi,opencode, ...), plusclaude-desktop,cloudflare-mcpandclaude-code.teamclaude, which other features list.git: Git defaults, aliases, signing, and ignore behaviour.cloud: Cloud SDK and CLI packages (AWS, Azure, GCP).containers: Linux rootless container prerequisites (newuidmap/newgidmapwrappers and nodocker marker).network: The systemd-networkd links of a system-manager host, and the LAN address that other features bind published container ports to.inference: A local model server, withollamaandopen-webuias children so a host can run one without the other.nixbuild-builder: nixbuild.net remote build configuration, including build-machine registration and cross-architecture build support.nixbuild: nixbuild.net SSH aliases and credentials for remote-store access and optional account administration.agentsviewandagentsview-server: the archive of agent sessions on a machine that runs agents, and the shared database behind it.hermes: the containerised Hermes agent and its messaging, archive, webhook, MCP, identity, and backup children. See the Hermes operator notes.adsb,caddy,dex,hermes,matrix,unifi: the services on ancaster.
The Neovim configuration is nvim/ at the repository root, which base.neovim
installs.
Hosts represent machines. A host record defines OS/architecture, the host's
features, and any configuration that applies to this host alone. Each file under
hosts/ is a flake-parts module that sets flake.hosts.<name>:
{config, ...}: let
inherit (config.flake) features;
in {
flake.hosts.example = {
hostname = "hostname.example.com";
os = "generic-linux";
arch = "x86_64";
motd = "Welcome to example";
features = [
features.base
features.desktop
features.development
features.cloud
features.work
];
# Configuration for this host alone
homeModule = {
programs.git.settings.user.email = "work@example.com";
};
};
}./bootstrap.shIt installs Determinate Nix, adds the sudo and admin groups to
trusted-users, and restarts the daemon.
We provide just targets. Run ./just --list to see what is available.
For normal maintenance, run:
./just updateThis refreshes the flake inputs, moving packages to their latest packaged
versions, deploys the current system, and pre-builds the direnv shells (see
above).
To deploy to a managed remote system, run ./just update-host <hostname>. To
deploy only the system or home deploy-rs profile, run
./just update-host-system <hostname> or ./just update-host-home <hostname>.
The individual steps can also be run separately.
Update flake inputs to their latest versions. Name inputs as arguments to update
only those, for example ./just update-flake llm-agents.
An input pinned to a release tag in its URL, such as hermes-agent, does not
move this way. update-pkgs moves those.
Run every registered updater. A package updater refreshes that package's source
metadata and any dependency pins it needs. A tag-pinned flake input's updater
moves the release tag in flake.nix and relocks that input.
./just update-pkg <name> runs a single one, for example
./just update-pkg chainctl or ./just update-pkg hermes-agent.
Update both system and home configuration.
Linux only. macOS is built with nix-darwin, whose darwin-rebuild always
updates system and home together.
Update home-manager user-level configuration only.
Pre-build direnv shells for all configured project directories. With them
built, the first cd into a project directory after a flake update does not
wait for its shell.
Run ./just fmt to check and fix formatting errors, ./just check to run
broader static analysis, and ./just lint to run them both.
A pre-commit hook also runs the Python checks for the prompt conformance
suite, which measures how a change to this repository's
assembled agent prompt affects real repository work. The hook requests a build
only when the commit touches an input to the checks, such as the instructions
and output styles under features/ai/, the flake inputs, or the suite itself.
Unchanged build inputs reuse the cached result.
To find packages, run ./just search <query> and ./just info <package>.
Try ./just why <package> and ./just deps <package> to trace why something is
installed. If a build fails, inspect logs with ./just log <package>, and open
a REPL with ./just repl for deeper investigation.
If an update goes wrong, inspect history with ./just generations, review
recent generations with ./just history 5, and compare two known generations
with ./just diff <gen1> <gen2>.
Some hosts in this repo run NixOS itself; the others use nix-darwin
or system-manager. A NixOS host's record in hosts/ sets os = "nixos".
Like everything else in this repo, these systems are declarative, but a full OS install has to be provisioned before it can be updated. The steps below cover that.
A new NixOS host needs cryptographic keys before it can decrypt secrets. Run:
./just generate-host-keys <host>The recipe generates the host's SSH and age keys and a user SSH key, and asks
you for the user's login password. It writes the new keys and the host's secrets
to the secrets repo, commits and pushes that repo, and then adds the user SSH
key to your GitHub account. scripts/generate-host-keys.bash lists each step.
If the target machine has no OS on disk yet, netboot a minimal NixOS installer
using pixiecore:
./just netboot <host>PXE-boot the target on the same network segment and find its IP. If the machine already has a live environment reachable over SSH, skip this step.
Once the target is reachable over SSH, install with
nixos-anywhere. Pass the keys directory printed by
generate-host-keys to inject them into the installed system:
./just install <host> <ip-or-hostname> /path/to/keys-dirThe keys are cleaned up locally after a successful install. The SSH host key is
installed at /etc/ssh/ssh_host_ed25519_key and the user age key at
~/.config/sops/age/keys.txt. Without a keys directory the install proceeds but
the host won't be able to decrypt secrets until keys are provided manually.
After the initial install, push configuration changes with:
./just update-host <host>Track disk usage growth with ./just size and ./just sizes, and delete
generations older than <days> with ./just gc <days> (defaults to 30).
Create hosts/HOSTNAME.nix to add a new host:
{config, ...}: {
flake.hosts.HOSTNAME = {
hostname = "hostname.example.com";
os = "darwin"; # or "generic-linux" (system-manager) or "nixos"
arch = "aarch64"; # or "x86_64"
motd = "Welcome to HOSTNAME";
features = [config.flake.features.base];
};
}A host with hardware or disk configuration of its own is a directory instead,
hosts/HOSTNAME/, whose default.nix imports the files beside it from its
systemModule. hosts/bonington/ is the example to copy.
Secrets are managed with sops-nix, which decrypts them at activation time using an age key derived from an SSH private key.
Key generation is handled by ./just generate-host-keys <host> as part of the
installation process. Both the SSH host key (system-level
decryption) and a dedicated user age key are generated and injected during
install. The user key is written in standard age format at
~/.config/sops/age/keys.txt.
On Darwin and Linux (system-manager) hosts, the simplest option is to generate a
standard age identity for sops:
mkdir -p ~/.config/sops/age
chmod 700 ~/.config/sops ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txtThen derive the age public key and add it to .sops.yaml in the
dotfiles-secrets repo so that secrets can be encrypted for this host:
nix shell nixpkgs#age -c age-keygen -y ~/.config/sops/age/keys.txtYou can also use an SSH private key that you already have as the age identity.
Convert it into keys.txt in place of the age-keygen -o step above:
mkdir -p ~/.config/sops/age
chmod 700 ~/.config/sops ~/.config/sops/age
nix shell nixpkgs#ssh-to-age -c sh -c 'ssh-to-age -private-key -i ~/.ssh/your_key >> ~/.config/sops/age/keys.txt'
chmod 600 ~/.config/sops/age/keys.txtThe private key material must never be committed or added to the Nix store.