<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Parker Jones Dev Blog - home-manager</title>
    <subtitle>Dev Blog of Parker Jones</subtitle>
    <link rel="self" type="application/atom+xml" href="https://parkerjones.dev/tags/home-manager/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://parkerjones.dev"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-26T00:00:00+00:00</updated>
    <id>https://parkerjones.dev/tags/home-manager/atom.xml</id>
    <entry xml:lang="en">
        <title>Nix Fleet, Part 3: Fleet-Wide Secrets with agenix</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/nix-fleet-agenix-secrets/"/>
        <id>https://parkerjones.dev/posts/nix-fleet-agenix-secrets/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/nix-fleet-agenix-secrets/">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nix Fleet, part 3.&lt;&#x2F;strong&gt; &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-one-flake&#x2F;&quot;&gt;Part 1&lt;&#x2F;a&gt; covered the architecture; &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-remote-builders&#x2F;&quot;&gt;Part 2&lt;&#x2F;a&gt; made builds painless. This part handles the thing you must &lt;em&gt;not&lt;&#x2F;em&gt; commit to a public config repo: secrets.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;My whole system is declared in a Git repository, and most of it is public. But a fleet needs secrets — API keys, SMB credentials, a WireGuard config — and those obviously can&#x27;t sit in plaintext next to the rest of the config. The constraint that shapes everything here: I want secrets to be &lt;strong&gt;versioned, shared across every machine, and reproducible&lt;&#x2F;strong&gt;, with the same Git-reviewable discipline as the rest of the system — without any secret ever appearing in plaintext, in the repo, or in the Nix store.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ryantm&#x2F;agenix&quot;&gt;agenix&lt;&#x2F;a&gt; gets me there. Here&#x27;s the setup.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;two-repos-one-boundary&quot;&gt;Two repos, one boundary&lt;&#x2F;h2&gt;
&lt;p&gt;The cleanest decision I made was to put secrets in their &lt;strong&gt;own separate repo&lt;&#x2F;strong&gt; (&lt;code&gt;nix-secrets&lt;&#x2F;code&gt;) and pull it into the system flake as an input. I keep my real one private, but I&#x27;ve published a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;nix-secrets&quot;&gt;public example repo&lt;&#x2F;a&gt; so you can see the shape:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;# flake.nix
inputs.secrets = {
  url = &amp;quot;git+ssh:&#x2F;&#x2F;git@github.com&#x2F;parallaxisjones&#x2F;nix-secrets.git&amp;quot;;
  flake = false;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That boundary pays off operationally: I can lock or revoke access to the secrets repo independently of the (public) config, rotate recipients without touching application code, and grant a machine read-only deploy access to just the secrets it needs. The public config can stay public because the sensitive bits live behind a separate door.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-agenix-encrypts&quot;&gt;How agenix encrypts&lt;&#x2F;h2&gt;
&lt;p&gt;agenix is &lt;code&gt;age&lt;&#x2F;code&gt; encryption wired into Nix. You declare, per secret, &lt;em&gt;which identities are allowed to decrypt it&lt;&#x2F;em&gt;. That declaration lives in &lt;code&gt;secrets.nix&lt;&#x2F;code&gt; in the private repo:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;let
  # macOS (pjones) and NixOS (parallaxis) decrypt with the
  # same ~&#x2F;.ssh&#x2F;parallaxis identity.
  pjones = &amp;quot;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...oqt7Aq&amp;quot;;
  users  = [ pjones ];
in
{
  &amp;quot;openai-key.age&amp;quot;.publicKeys        = users;
  &amp;quot;anthropic-api-key.age&amp;quot;.publicKeys = users;
  &amp;quot;datadog-api-key.age&amp;quot;.publicKeys   = users;
  &amp;quot;datadog-app-key.age&amp;quot;.publicKeys   = users;
  &amp;quot;smb-credentials.age&amp;quot;.publicKeys   = users;
  &amp;quot;protonvpn-wireguard.age&amp;quot;.publicKeys = users;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Each &lt;code&gt;.age&lt;&#x2F;code&gt; file is encrypted to the public keys listed. The matching private identity — and &lt;em&gt;only&lt;&#x2F;em&gt; that identity — can decrypt it. The encrypted files are safe to commit (to the private repo); the identity never is.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;one-identity-for-the-whole-fleet&quot;&gt;One identity for the whole fleet&lt;&#x2F;h2&gt;
&lt;p&gt;Notice that &lt;code&gt;users&lt;&#x2F;code&gt; is a single key. That&#x27;s deliberate: &lt;strong&gt;both my macOS and NixOS machines decrypt with the same &lt;code&gt;~&#x2F;.ssh&#x2F;parallaxis&lt;&#x2F;code&gt; ed25519 identity.&lt;&#x2F;strong&gt; agenix can use an &lt;code&gt;age&lt;&#x2F;code&gt;-native key (&lt;code&gt;age-keygen&lt;&#x2F;code&gt;) or reuse an existing SSH ed25519 key via &lt;code&gt;age-plugin-ssh&lt;&#x2F;code&gt;; I reuse the SSH key so there&#x27;s exactly one secret-decrypting identity to manage across the fleet.&lt;&#x2F;p&gt;
&lt;p&gt;The trade-off is real and worth stating: one identity is one thing to protect and one thing to rotate, but it&#x27;s also a single point of compromise. For a personal fleet that&#x27;s the right call — fewer keys means I actually keep the rotation story straight instead of letting five per-host keys drift. If this were a team, I&#x27;d encrypt each secret to multiple recipients (one per person&#x2F;host) so revoking one doesn&#x27;t force re-keying everyone. agenix supports exactly that; it&#x27;s just a longer &lt;code&gt;publicKeys&lt;&#x2F;code&gt; list.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-decryption-works-at-runtime&quot;&gt;How decryption works at runtime&lt;&#x2F;h2&gt;
&lt;p&gt;This is the part that makes agenix better than &quot;stash a &lt;code&gt;.env&lt;&#x2F;code&gt; somewhere.&quot; At activation time, NixOS&#x2F;nix-darwin decrypts each declared secret to a path under &lt;code&gt;&#x2F;run&lt;&#x2F;code&gt; (tmpfs), owned by the user&#x2F;service that needs it, with the permissions you specify. The decrypted value:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;never lands in the Nix store&lt;&#x2F;strong&gt; (which is world-readable and gets copied to your binary cache),&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;never sits in the repo&lt;&#x2F;strong&gt; in plaintext,&lt;&#x2F;li&gt;
&lt;li&gt;exists only on the machine authorized to decrypt it, only while the system is running.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So a service reads its API key from a file at &lt;code&gt;&#x2F;run&#x2F;agenix&#x2F;...&lt;&#x2F;code&gt;, that file only exists because &lt;em&gt;this&lt;&#x2F;em&gt; machine holds the identity that can decrypt it, and the policy for who-can-read-what is reviewable in &lt;code&gt;secrets.nix&lt;&#x2F;code&gt;. Secrets management becomes declarative and auditable, like the rest of the config.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bootstrapping-a-brand-new-machine&quot;&gt;Bootstrapping a brand-new machine&lt;&#x2F;h2&gt;
&lt;p&gt;The chicken-and-egg problem: a fresh machine needs secrets to be useful, but doesn&#x27;t yet have the identity to decrypt them. My flake exposes helper apps for exactly this lifecycle:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;nix run .#create-keys          # generate keys on a new machine
nix run .#copy-keys            # place an identity where agenix expects it
nix run .#check-keys           # verify the identity can decrypt
nix run .#install-with-secrets # first install WITH secrets provisioned
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;install-with-secrets&lt;&#x2F;code&gt; is the interesting one. On a first NixOS install it runs &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-helios64-disko&#x2F;&quot;&gt;disko&lt;&#x2F;a&gt; to partition, stages the repo to the new root, and installs via the flake — and crucially, it relies on &lt;strong&gt;SSH agent forwarding&lt;&#x2F;strong&gt; so the installer can fetch the private &lt;code&gt;secrets&lt;&#x2F;code&gt; input without ever copying a key onto the installer media. Nothing persistent is left behind on the installer once it&#x27;s done. The machine comes up on first boot already matching repo state, with its secrets provisioned where they&#x27;re declared.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-this-beats-the-alternatives&quot;&gt;Why this beats the alternatives&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vs. plaintext &lt;code&gt;.env&lt;&#x2F;code&gt; &#x2F; committed config:&lt;&#x2F;strong&gt; secrets are encrypted at rest, never in the repo, never in the store.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;vs. a cloud secrets manager:&lt;&#x2F;strong&gt; no external dependency, no per-machine bootstrapping against a third-party API, and the policy is in Git where I review everything else.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;vs. copying keys around by hand:&lt;&#x2F;strong&gt; the recipient list is declarative; adding or removing who-can-decrypt is a reviewable diff, not a tribal-knowledge ritual.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The throughline of this whole series is &quot;manage it declaratively, in Git, reproducibly.&quot; Secrets are the case where people usually give up and do something ad-hoc. agenix is how I kept them inside the same discipline as everything else. Next, in &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-helios64-disko&#x2F;&quot;&gt;Part 4&lt;&#x2F;a&gt;, I put all of this to work standing up an ARM NAS from bare metal — disko for the disks, agenix for the backup credentials, and &lt;code&gt;nixos-anywhere&lt;&#x2F;code&gt; to tie it together.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;A public example secrets repo is at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;nix-secrets&quot;&gt;&lt;code&gt;parallaxisjones&#x2F;nix-secrets&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;; the fleet config that consumes it is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;dotfiles&quot;&gt;&lt;code&gt;parallaxisjones&#x2F;dotfiles&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>One Flake, Every Machine: a NixOS + macOS Fleet</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/nix-fleet-one-flake/"/>
        <id>https://parkerjones.dev/posts/nix-fleet-one-flake/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/nix-fleet-one-flake/">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nix Fleet, part 1.&lt;&#x2F;strong&gt; This post is the architecture overview. Later parts go deep on the pieces it depends on: cross-arch remote builders, fleet-wide secrets with agenix, and bare-metal install of a NAS with disko.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Every machine I work on — Linux servers, a NAS, and my M3 MacBook — is configured by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;dotfiles&quot;&gt;one Git repository&lt;&#x2F;a&gt;. One &lt;code&gt;flake.nix&lt;&#x2F;code&gt; describes all of them: same shell, same tools, same agent skills, same secrets handling, whether the target is &lt;code&gt;x86_64-linux&lt;&#x2F;code&gt;, &lt;code&gt;aarch64-linux&lt;&#x2F;code&gt;, or &lt;code&gt;aarch64-darwin&lt;&#x2F;code&gt;. Adding a machine means adding a host entry; changing my setup everywhere means one commit and a rebuild.&lt;&#x2F;p&gt;
&lt;p&gt;This is the &quot;single source of truth for my computing environment&quot; that Nix promises and rarely delivers without a fight. Here&#x27;s how the repo is laid out and the one trick that makes cross-architecture management actually pleasant.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-shape-of-the-repo&quot;&gt;The shape of the repo&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;nixos-config&#x2F;
├── flake.nix          # inputs + outputs: hosts, devShells, apps
├── hosts&#x2F;
│   ├── nixos&#x2F;         # configuration.nix, hardware-configuration.nix,
│   │                  # helios64.nix (the NAS), profiles&#x2F;
│   └── darwin&#x2F;        # configuration.nix (the Mac)
├── modules&#x2F;
│   ├── shared&#x2F;        # config + secrets shared across OSes
│   ├── nixos&#x2F;         # homelab services, Linux-only config
│   └── darwin&#x2F;        # dock, homebrew casks, home-manager
├── overlays&#x2F;          # package customizations
└── justfile           # deploy &#x2F; check convenience targets
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The split that makes this maintainable: &lt;strong&gt;&lt;code&gt;hosts&#x2F;&lt;&#x2F;code&gt; is per-machine, &lt;code&gt;modules&#x2F;&lt;&#x2F;code&gt; is shared.&lt;&#x2F;strong&gt; A host file is mostly a list of which modules to import plus its hardware specifics. The actual configuration — my packages, dotfiles, services — lives in modules that any host can pull in. When I want a tool on every machine, it goes in &lt;code&gt;modules&#x2F;shared&lt;&#x2F;code&gt;; when it&#x27;s Linux-only, &lt;code&gt;modules&#x2F;nixos&lt;&#x2F;code&gt;; macOS-only, &lt;code&gt;modules&#x2F;darwin&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-s-wired-together&quot;&gt;What&#x27;s wired together&lt;&#x2F;h2&gt;
&lt;p&gt;The flake stitches together the whole modern Nix ecosystem:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;inputs = {
  nixpkgs.url = &amp;quot;github:nixos&#x2F;nixpkgs&#x2F;nixos-unstable&amp;quot;;
  home-manager.url = &amp;quot;github:nix-community&#x2F;home-manager&amp;quot;;
  darwin.url = &amp;quot;github:LnL7&#x2F;nix-darwin&#x2F;master&amp;quot;;   # macOS, declaratively
  nix-homebrew.url = &amp;quot;github:zhaofengli-wip&#x2F;nix-homebrew&amp;quot;;
  disko.url = &amp;quot;github:nix-community&#x2F;disko&amp;quot;;        # declarative partitioning
  fenix.url = &amp;quot;github:nix-community&#x2F;fenix&amp;quot;;         # Rust toolchains
  agenix.url = &amp;quot;github:ryantm&#x2F;agenix&amp;quot;;              # age-encrypted secrets
  secrets.url = &amp;quot;git+ssh:&#x2F;&#x2F;git@github.com&#x2F;parallaxisjones&#x2F;nix-secrets.git&amp;quot;;
  my-skills.url = &amp;quot;github:parallaxisjones&#x2F;skills&amp;quot;;  # my Claude skills
  # ...
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A few deliberate choices in there:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nix-darwin&lt;&#x2F;code&gt; + &lt;code&gt;home-manager&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; mean my Mac is configured by the same mechanisms as my Linux boxes. The dock, defaults, and dotfiles are all declarative.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nix-homebrew&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; is the pragmatic concession: GUI Mac apps still come from Homebrew casks, but managed &lt;em&gt;through&lt;&#x2F;em&gt; Nix so the cask list is in the repo, not in my shell history.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fenix&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; pins my Rust toolchain so &lt;code&gt;cargo&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;clippy&lt;&#x2F;code&gt; are identical everywhere.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;secrets&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; is a &lt;em&gt;separate private repo&lt;&#x2F;em&gt; pulled in as an input — encrypted secrets are versioned and shared across hosts without living in this (public) config. That&#x27;s its own post.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;my-skills&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; wires my &lt;a href=&quot;&#x2F;posts&#x2F;skills-with-nix&#x2F;&quot;&gt;Claude agent skills&lt;&#x2F;a&gt; into the same declarative system.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-cross-arch-trick-build-where-you-can&quot;&gt;The cross-arch trick: build where you can&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s the problem a mixed fleet runs into immediately: &lt;strong&gt;my macOS laptop cannot build Linux derivations locally.&lt;&#x2F;strong&gt; So how do I deploy to the NixOS box from the Mac? The answer is to build &lt;em&gt;on the target&lt;&#x2F;em&gt; and just orchestrate from the laptop. My &lt;code&gt;justfile&lt;&#x2F;code&gt; deploy recipe:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;make&quot;&gt;# Build + switch the NixOS host remotely over SSH. Builds ON the server
# (--build-host) since a darwin laptop can&amp;#39;t build Linux derivations locally.
deploy host=nixos_host:
    nix run nixpkgs#nixos-rebuild -- switch \
      --flake .#{{nixos_attr}} \
      --target-host parallaxis@{{host}} \
      --build-host  parallaxis@{{host}} \
      --use-remote-sudo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--build-host&lt;&#x2F;code&gt; and &lt;code&gt;--target-host&lt;&#x2F;code&gt; both pointing at the server means the laptop never compiles a Linux package — it hands the flake to the server, the server builds and switches, and &lt;code&gt;--use-remote-sudo&lt;&#x2F;code&gt; handles the privilege step. I get to drive everything from my editor on macOS. (Part 2 covers the inverse — building Linux artifacts &lt;em&gt;from&lt;&#x2F;em&gt; the Mac via a remote builder — for when you do want local orchestration of the build itself.)&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s a &lt;code&gt;check&lt;&#x2F;code&gt; target too, for a fast feedback loop that doesn&#x27;t build anything:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;make&quot;&gt;check:
    nix eval .#nixosConfigurations.{{nixos_attr}}.config.system.build.toplevel.drvPath
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Evaluating the derivation path is a near-instant syntax-and-type check of the whole host config — the Nix equivalent of &lt;code&gt;tsc --noEmit&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-gotcha-that-cost-me-time&quot;&gt;The gotcha that cost me time&lt;&#x2F;h2&gt;
&lt;p&gt;My &lt;code&gt;nixosConfigurations&lt;&#x2F;code&gt; are keyed by &lt;strong&gt;system string&lt;&#x2F;strong&gt;, not hostname. So the deploy target is &lt;code&gt;.#x86_64-linux&lt;&#x2F;code&gt;, &lt;em&gt;not&lt;&#x2F;em&gt; &lt;code&gt;.#nixos&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;make&quot;&gt;nixos_attr := &amp;quot;x86_64-linux&amp;quot;   # NOT the hostname
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you key your configs this way and then try &lt;code&gt;nixos-rebuild --flake .#nixos&lt;&#x2F;code&gt;, you get a confusing &quot;attribute not found&quot; with no hint about why. Worth a comment in your own repo so future-you doesn&#x27;t lose twenty minutes to it. (I did.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-it-s-worth-the-upfront-cost&quot;&gt;Why it&#x27;s worth the upfront cost&lt;&#x2F;h2&gt;
&lt;p&gt;Nix has a real learning tax, and a single-machine setup rarely justifies it. A &lt;em&gt;fleet&lt;&#x2F;em&gt; does. The payoffs that keep me here:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Small, frequent, reversible changes.&lt;&#x2F;strong&gt; Every change is a commit; every rebuild is a new generation I can roll back to atomically. I deploy config changes the way I deploy code.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;New machines reach parity from the flake.&lt;&#x2F;strong&gt; No &quot;setup day.&quot; The repo &lt;em&gt;is&lt;&#x2F;em&gt; the setup.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;One mental model across operating systems.&lt;&#x2F;strong&gt; I stopped maintaining a separate pile of macOS hacks and a separate pile of Linux hacks. It&#x27;s all modules.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The roadmap from here — and the next posts in this series — is the interesting infrastructure underneath: &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-remote-builders&#x2F;&quot;&gt;&lt;strong&gt;remote builders&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; so cross-compilation stops being a chore (part 2), &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-agenix-secrets&#x2F;&quot;&gt;&lt;strong&gt;agenix&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; for secrets that are versioned and fleet-wide without ever sitting in plaintext (part 3), and standing up a &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-helios64-disko&#x2F;&quot;&gt;&lt;strong&gt;Helios64 NAS&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; from bare metal with &lt;code&gt;disko&lt;&#x2F;code&gt; (part 4). The overview is the boring part. The machinery is where Nix gets fun.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The full fleet configuration lives in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;dotfiles&quot;&gt;&lt;code&gt;parallaxisjones&#x2F;dotfiles&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Shipping Claude Skills with Nix: a Reproducible Agent Toolkit Across My Fleet</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/skills-with-nix/"/>
        <id>https://parkerjones.dev/posts/skills-with-nix/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/skills-with-nix/">&lt;p&gt;A coding agent is only as good as the procedures you hand it. Out of the box, a model improvises every task from scratch; give it a &lt;em&gt;skill&lt;&#x2F;em&gt; — a written procedure for &quot;do TDD,&quot; &quot;diagnose a hard bug,&quot; &quot;turn this into issues&quot; — and it stops guessing and starts following a method you trust. I&#x27;ve built up a collection of these, and once you have more than a handful the real problem isn&#x27;t writing them, it&#x27;s &lt;strong&gt;distribution&lt;&#x2F;strong&gt;: getting the same skills onto every machine I work from, version-pinned, without copy-pasting Markdown around.&lt;&#x2F;p&gt;
&lt;p&gt;I solved that with Nix. Here&#x27;s the setup.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-a-skill-is&quot;&gt;What a skill is&lt;&#x2F;h2&gt;
&lt;p&gt;My skills live in a repo (a fork of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mattpocock&#x2F;skills&quot;&gt;Matt Pocock&#x27;s &lt;code&gt;skills&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, credit where it&#x27;s due), organized into buckets — &lt;code&gt;engineering&lt;&#x2F;code&gt;, &lt;code&gt;qa&lt;&#x2F;code&gt;, &lt;code&gt;productivity&lt;&#x2F;code&gt;, &lt;code&gt;personal&lt;&#x2F;code&gt;, &lt;code&gt;misc&lt;&#x2F;code&gt;. Each skill is a directory with a &lt;code&gt;SKILL.md&lt;&#x2F;code&gt;: YAML frontmatter naming it and describing &lt;em&gt;when&lt;&#x2F;em&gt; to use it, then the procedure itself.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;markdown&quot;&gt;---
name: diagnose
description: Disciplined diagnosis loop for hard bugs and performance
  regressions. Reproduce → minimise → hypothesise → instrument → fix →
  regression-test. Use when user says &amp;quot;diagnose this&amp;quot; &#x2F; &amp;quot;debug this&amp;quot;...
---

# Diagnose
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;description&lt;&#x2F;code&gt; is load-bearing — it&#x27;s what the agent matches against to decide whether the skill is relevant. A few I reach for constantly: &lt;code&gt;tdd&lt;&#x2F;code&gt; (red-green-refactor), &lt;code&gt;diagnose&lt;&#x2F;code&gt; (the loop above), &lt;code&gt;to-prd&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;to-issues&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;triage&lt;&#x2F;code&gt; (turning a vague ask into tracked work), and &lt;code&gt;write-a-skill&lt;&#x2F;code&gt; (the skill that writes more skills). Small, composable, model-agnostic. No framework owning my process — just procedures I can read, edit, and trust.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;three-ways-to-install-them&quot;&gt;Three ways to install them&lt;&#x2F;h2&gt;
&lt;p&gt;The repo supports three distribution paths, in increasing order of how much I actually rely on them.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;npx&lt;&#x2F;code&gt;, for anyone.&lt;&#x2F;strong&gt; The zero-commitment path:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;npx skills@latest add parallaxisjones&#x2F;skills
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Pick the skills and agents you want, and you&#x27;re set. Great for trying them on a machine that isn&#x27;t mine.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. A symlink script, for a local clone.&lt;&#x2F;strong&gt; If I&#x27;ve cloned the repo, &lt;code&gt;link-skills.sh&lt;&#x2F;code&gt; symlinks every &lt;code&gt;SKILL.md&lt;&#x2F;code&gt; into &lt;code&gt;~&#x2F;.claude&#x2F;skills&#x2F;&lt;&#x2F;code&gt; so the CLI picks them up directly. It&#x27;s idempotent — re-run after pulling — and it specifically guards against the footgun where &lt;code&gt;~&#x2F;.claude&#x2F;skills&lt;&#x2F;code&gt; is &lt;em&gt;itself&lt;&#x2F;em&gt; a symlink back into the repo, which would write per-skill symlinks into my own working copy:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;if [ -L &amp;quot;$DEST&amp;quot; ]; then
  resolved=&amp;quot;$(readlink -f &amp;quot;$DEST&amp;quot;)&amp;quot;
  case &amp;quot;$resolved&amp;quot; in
    &amp;quot;$REPO&amp;quot;&#x2F;*) echo &amp;quot;refusing to pollute the repo&amp;quot;; exit 1 ;;
  esac
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That defensive check is the kind of thing you write &lt;em&gt;after&lt;&#x2F;em&gt; the first time a script eats its own tail.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3. Nix, for my actual fleet.&lt;&#x2F;strong&gt; This is the one that matters. My skills repo is a flake input in my &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-one-flake&#x2F;&quot;&gt;system config&lt;&#x2F;a&gt;, and a home-manager module materializes them declaratively on every machine.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-nix-wiring&quot;&gt;The Nix wiring&lt;&#x2F;h2&gt;
&lt;p&gt;Two inputs do the work — my skills repo, and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Kyure-A&#x2F;agent-skills-nix&quot;&gt;&lt;code&gt;agent-skills-nix&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, a home-manager module that knows how to turn a skills repo into materialized files:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;# flake.nix
inputs = {
  agent-skills-nix = {
    url = &amp;quot;github:Kyure-A&#x2F;agent-skills-nix&amp;quot;;
    inputs.nixpkgs.follows = &amp;quot;nixpkgs&amp;quot;;
    inputs.home-manager.follows = &amp;quot;home-manager&amp;quot;;
  };
  my-skills.url = &amp;quot;github:parallaxisjones&#x2F;skills&amp;quot;;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then in home-manager I point the module at my repo, filter to the buckets I want live, and enable everything:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;agent-skills = {
  enable = true;
  sources.mine = {
    input  = &amp;quot;my-skills&amp;quot;;
    subdir = &amp;quot;skills&amp;quot;;
    filter.nameRegex = &amp;quot;^(engineering|misc|personal|productivity)&#x2F;.*&amp;quot;;
  };
  skills.enableAll = true;
  targets.claude.enable = true;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That&#x27;s the whole thing. On &lt;code&gt;nixos-rebuild switch&lt;&#x2F;code&gt; (or &lt;code&gt;darwin-rebuild&lt;&#x2F;code&gt; on the Mac), every &lt;code&gt;SKILL.md&lt;&#x2F;code&gt; matching the regex gets written into Claude&#x27;s skills directory. The &lt;code&gt;deprecated&#x2F;&lt;&#x2F;code&gt; bucket is excluded by the filter, so retiring a skill is a one-line regex change, not a manual delete on five machines.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-bother-what-nix-actually-buys-here&quot;&gt;Why bother — what Nix actually buys here&lt;&#x2F;h2&gt;
&lt;p&gt;You could argue the symlink script is simpler, and for one machine it is. The fleet is where Nix earns it:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pinning.&lt;&#x2F;strong&gt; &lt;code&gt;flake.lock&lt;&#x2F;code&gt; records the exact skills commit each machine is on. &quot;Which version of my &lt;code&gt;triage&lt;&#x2F;code&gt; skill is the laptop running?&quot; has an answer, not a shrug.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Parity from scratch.&lt;&#x2F;strong&gt; A fresh machine reaches full skill parity as a side effect of building its system config. There&#x27;s no separate &quot;and don&#x27;t forget to install your skills&quot; step — it&#x27;s the same &lt;code&gt;switch&lt;&#x2F;code&gt; that installs my shell and my packages.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Atomic rollback.&lt;&#x2F;strong&gt; If a skill edit makes the agent behave worse, rolling back the generation rolls back the skills with it.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;There&#x27;s a framing I lean on for deciding what to manage this way — think of three zones. &lt;em&gt;Zone 1&lt;&#x2F;em&gt; is stable, declarative config (the home-manager module enabling skills). &lt;em&gt;Zone 3&lt;&#x2F;em&gt; is runtime state that should never touch Nix (per-conversation agent memory). Skills sit in &lt;strong&gt;Zone 2&lt;&#x2F;strong&gt;: authored content, edited as plain files in their repo, but &lt;em&gt;materialized&lt;&#x2F;em&gt; onto each machine by Nix. Nix owns where they land and which version; I own what they say.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-honest-caveat&quot;&gt;The honest caveat&lt;&#x2F;h2&gt;
&lt;p&gt;Nix doesn&#x27;t validate skill &lt;em&gt;content&lt;&#x2F;em&gt; — a &lt;code&gt;SKILL.md&lt;&#x2F;code&gt; with a bad &lt;code&gt;description&lt;&#x2F;code&gt; will deploy just as reliably as a good one. This pipeline guarantees distribution and pinning, not quality. The quality comes from treating skills like code: review them, iterate on the descriptions when the agent picks the wrong one, and delete the ones that stop earning their place (that&#x27;s what &lt;code&gt;deprecated&#x2F;&lt;&#x2F;code&gt; is for).&lt;&#x2F;p&gt;
&lt;p&gt;But that&#x27;s the right division of labor. Writing a good procedure is human work. Making sure that procedure is &lt;em&gt;identically present on every machine I touch&lt;&#x2F;em&gt; is exactly the kind of toil Nix exists to kill. Treat your agent&#x27;s skills as part of your declarative system, not as dotfiles you sync by hand.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
