BIRKEY CONSULTING

ABOUT  RSS  ARCHIVE


Posts tagged "nix":

19 Apr 2026

Why I Love Agent Pi

Pi1 is the first coding agent that really empowered me to morph it to fit the way my brain works, and I am genuinely excited about how malleable it is on top of a handful of primitives.

Most coding agents I have tried in the past have grown in the same direction. More built-in tools. More opinionated prompts. More hidden, opaque decisions with no real visibility or control. More assumptions that I will bend my workflow around theirs - mandatory hooks here, implicit conventions there - instead of letting me configure a few simple things and get on with my work.

The small stuff gives it away. A recent example: Claude Code animates its "thinking" state with a glyph that lives in a font not every machine has. If you happen to use a font that does not have extensive glyph support, it makes eterm2 trigger a redraw on every tick, resulting in the surrounding text shaking visually. Yes, I can switch to a different font, but I did not have the choice to turn it off. It is a small thing. It is also exactly the kind of small thing you cannot fix from the outside, because the decision is buried somewhere you are not invited to touch. You start to succumb to their way of doing things. Small things aside, I cannot tell what the agent is really doing, I cannot tell why it chose a particular path, and I cannot tell how to change its behavior without jumping through the hoops they put in place.

Pi does almost the opposite. Its core is four primitives: read, write, edit, and bash. That is roughly it. Everything else is something you compose on top, or an extension you opt into. That small surface is exactly the thing I keep coming back for.

If you want a fuller sense of the thinking behind Pi, I would point you to a twenty-minute talk by its creator3. I do not often recommend agent-related talks - most of them age badly within a week or so - but the way he frames agents, agentic coding, and the current landscape is the clearest version of that picture I have heard recently. It is worth your time even if you never use Pi.

Small primitives, visible behavior

A coding agent with four primitives is not a limitation in the way it first appears. Most of what a coding agent actually needs to do boils down to reading a file, writing a file, changing part of a file, or running a command. If you can see each of those happen, you can reason about what the agent is doing. If you can script around them, you can make the agent behave the way your project already behaves.

This matters to me because I spend a lot of my time with agents trying to get clarity - building a clearer mental model of a problem before I commit to a direction. That work depends on visibility. What did the agent change? What did it run? What did it see? An agent whose behavior reduces to a handful of visible primitives is much easier to think alongside than one that hides its moves behind a thicker abstraction.

I do not think this is a new insight. Unix made the same bet a long time ago. So did Emacs, in its own way, with text, buffers, processes, and commands. The bet is that a small number of durable primitives plus a programmable substrate will outlast whatever feature-of-the-month the industry is currently excited about. Pi feels like that bet applied to coding agents.

Where Pi fits in the way I already work

I did not come to Pi looking for a new philosophy. I came to it because it fits two things I already depend on.

The first is Nix. I have written before about why I lean on it heavily4. In short, I do not want coding agents mutating my machine. I want them to declare what they need, pull it into an isolated shell, use it, and leave no residue. Pi cooperates with this model very naturally. Because its only way to reach the outside world is the shell primitive, I can point it at nix shell or nix develop and it will happily do real work inside a clean environment. A Rust toolchain, a specific Python, an odd CLI tool for one task - none of it has to touch my base system. When I am done, nothing is left behind except, if I want, a flake.nix that captures what actually worked.

The second is Emacs. I have also written about treating Emacs as a programmable workbench5. A coding agent that respects shell and files, and does not insist on its own fancy UI, drops into that workbench without friction. Pi runs in a buffer. As a simple example, I can make it interact with Emacs buffers to persist the commands it ran, so I can come back to them later if I need to.

Those two fits are not accidents of Pi's implementation. They fall out of the decision to keep the primitives small and the surface inspectable. An agent that tried to be a full IDE would not compose with Nix or Emacs this cleanly, because it would be busy being its own environment.

Extensibility I can actually use

The other thing I like about Pi is that its extension points are honest. When I want the agent to behave differently on a particular project, I do not have to reverse-engineer a hidden system prompt or wait for a vendor to ship a feature. I can add an extension, adjust a skill, or wire in a custom tool, and the change is visible in the same place the rest of the behavior lives.

Here is a concrete example. I wanted a small workflow for myself: whenever I ask one model to brainstorm an idea with me, a second comparable model should immediately review the response and list pros and cons, and optionally a third arbitrator model should weigh in when the two disagree. Useful pattern, but not something any agent I know ships out of the box. I asked Pi to build it for me as a peer-review extension. It did. Now, my brainstorming sessions work exactly as I wanted.

What made that work was not that Pi is clever. It was that Pi ships with documentation and code examples for its own extension points, and those examples are legible enough that the agent could read them and write a correct extension on the first try. I did not reverse-engineer anything. I did not hunt down a third-party tutorial. I just asked Pi to make the extension and it built the thing.

That is the part I keep thinking about. Pi is self-documenting in the same sense that Emacs is self-documenting6 - the system knows how to explain itself, to me and to any agent I put in front of it. That property sounds almost unremarkable until you notice how rare it is in modern tooling, and how much friction its absence quietly adds to every customization you try to make.

The honest trade-offs

Pi is not the right answer for everyone, and I do not want to oversell it.

Out of the box, it is minimal. If you want an agent that arrives pre-wired with a dozen integrations and a confident opinion about how your project should be structured, Pi will feel underdressed. You are expected to bring the way you think, work, and taste yourself. That takes some getting used to. It is the same kind of work Nix and Emacs ask for, and it pays back in the same way.

It is also a young project. Rough edges exist. Pi is not the only agent in this minimalist neighborhood either - Aider and OpenCode share a lot of the same instincts, and I think that is healthy. The argument I am making is not that Pi has won. It is that the design pressure behind Pi is the one I want more agents to feel.

Putting my money where my mouth is

At some point I realized I had been talking about Pi's design philosophy enough that I should probably just internalize it the hard way: by building something that takes the same bet from scratch. So I did. I wrote a small coding agent in Rust called OneLoop7. Its core is exactly what this post has been describing: four tools (read, write, edit, bash), one agent loop, and a session model that appends linearly to a JSONL file. Nothing else.

I am not bringing this up to announce a project. I am bringing it up because it proved something to me. OneLoop is rough, young, and does almost nothing compared to any "serious" coding agent on the market. And yet I am using it right now - in fact, the flake.nix in the OneLoop repo itself was written by OneLoop. The agent is building its own infrastructure. It works for real work because the model does the heavy lifting, and the agent's only job is to stay out of the way: hand it files, hand it a bash shell, and let it run. When your agent is just primitives and a loop, you do not need much more than that to be productive.

Building it also taught me something I did not fully appreciate from just using Pi. The hard part of a minimalist coding agent is not the agent loop - that is genuinely small. The hard part is everything around it: truncation heuristics so the model context does not explode, sensible output formatting so you can see what happened, session persistence so you can pick up where you left off. Those are real engineering problems. But they are infrastructure problems, not agent design problems, and it is useful to know the difference. Pi absorbs those infrastructure decisions so you do not have to think about them. OneLoop forced me to confront them directly. Both experiences are valuable.

Why this matters beyond Pi

The reason I wanted to write this down is not really about one tool. It is about a pattern I keep noticing in the parts of my setup that have aged well.

The systems I still rely on after many years tend to share a shape. A small number of durable primitives. A programmable substrate. Honest extension points. A willingness to be boring where boring is the right answer. NixOS has that shape at the system level. Emacs has it at the workbench level. Pi showed it to me at the agent level. OneLoop is my way of making sure I actually understand it.

I do not know which specific tools I will be using in five years. I do know that the ones that survive, for me, will look more like this and less like the feature-heavy alternatives that dominate the current moment. That is the real reason I love Pi. Not because it is the final answer, but because it is built in a shape that can keep being useful while the rest of the landscape keeps adding bloated features until it works for everyone - maybe not for you or me.

Footnotes:

1

Pi is a minimal, extensible coding agent by Mario Zechner: https://pi.dev/.

2

eterm is my fork of EAT, a pure Emacs Lisp terminal emulator, modified to fit my workflow.

3

Mario Zechner's talk on agents and the current agentic coding landscape: https://www.youtube.com/watch?v=RjfbvDXpFls.

4

My earlier post on why I rely on Nix, especially in the LLM coding era: https://www.birkey.co/2026-03-22-why-i-love-nixos.html.

5

My earlier post on Emacs as a programmable workbench: https://www.birkey.co/2026-03-28-emacs-as-a-programmable-workbench.html.

6

The GNU Emacs manual describes Emacs as a "self-documenting" editor and explains that this means you can use help commands at any time to find out what your options are and what commands do: https://www.gnu.org/software/emacs/manual/html_node/emacs/Intro.html.

7

OneLoop is a tiny coding agent written in Rust. It is a private repo for now. I will open-source it at some point when I believe it is ready.

Tags: nix emacs coding-agent ai
22 Mar 2026

Why I love NixOS

What I love about NixOS has less to do with Linux and more to do with the Nix package manager1.

To me, NixOS is the operating system artifact of a much more important idea: a deterministic and reproducible functional package manager. That is the core of why I love NixOS. It is not distro branding that I care about. It is the fact that I can construct a whole operating system as a deterministic result of feeding Nix DSL to Nix and then rebuild it, change it bit by bit, and roll it back if I do not like the result.

I love NixOS because most operating systems slowly turn into a pile of state. You install packages, tweak settings, try random tools, remove some of them, upgrade over time and after a while you have a machine that works but not in a way that you can confidently explain from first principles. NixOS felt very different to me. I do not have to trust a pile of state. I can define a system and build it.

I love NixOS because I can specify the whole OS including the packages I need and the configuration in one declarative setup. That one place aspect matters to me more than it might sound at first. I do not have to chase package choices in one place, desktop settings in another place and keyboard behavior somewhere else. Below are a couple of small Nix DSL examples.

environment.systemPackages = with pkgs; [
  gnomeExtensions.dash-to-dock
  gnomeExtensions.unite
  gnomeExtensions.appindicator
  libappindicator
];

services.desktopManager.gnome.extraGSettingsOverrides = ''
  [org.gnome.shell]
  enabled-extensions=['dash-to-dock@gnome-shell-extensions.gcampax.github.com', 'unite@hardpixel.eu', 'appindicatorsupport@rgcjonas.gmail.com']

  [org.gnome.shell.extensions.dash-to-dock]
  dock-position='BOTTOM'
  autohide=true
  dock-fixed=false
  extend-height=false
  transparency-mode='FIX'
'';
services.keyd = {
  enable = true;

  keyboards = {
    usb_keyboard = {
      ids = [ "usb:kb" ];
      settings.main = {
        leftcontrol = "leftmeta";
        leftmeta = "leftcontrol";
        rightalt = "rightmeta";
        rightmeta = "rightalt";
      };
    };

    laptop_keyboard = {
      ids = [ "laptop:kb" ];
      settings.main = swapLeftAltLeftControl;
    };
  };
};

Those are ordinary details of a working machine, but that is exactly the point. I can describe them declaratively, rebuild the system and keep moving. If I buy a new computer, I do not have to remember a long chain of manual setup steps or half-baked scripts scattered all over. I can rebuild the system from a single source of truth.

I love NixOS because it has been around for a long time. In my experience, it has been very stable. It has a predictable release cadence every six months. I can set it up to update automatically and upgrade it without the usual fear that tends to come with operating system upgrades. I do not have to think much about upgrade prompts, desktop notifications or random system drift in the background. It mostly stays out of my way. And if I want to be more adventurous, it also has an unstable channel2 that I can enable to experiment and get newer software.

I love NixOS because it lets my laptop be boring in the best possible sense. I recently bought an HP laptop3 and NixOS worked beautifully on it out of the box. I did not have to fight the hardware to get to a reasonable baseline. That gave me exactly what I want from a personal computer: a stable system that I can configure declaratively and then mostly ignore while I focus on actual work.

I love NixOS because it makes experimentation cheap and safe. I can try packages without mutating the base system. I can construct a completely isolated package shell4 for anything from a one-off script to a full-blown project. If I want to harden it further, I can use the Nix DSL to specify the dependencies, build steps and resulting artifacts declaratively. That is a much better way to work than slowly polluting my daily driver and hoping I can reconstruct what I did later.

I love NixOS because I can use the same package manager across macOS and Linux. There is also community-maintained support for FreeBSD, though I have not used it personally. That is a huge practical benefit because my development tooling and dependency management can stay mostly uniform across those systems. It means the value of Nix is not tied only to NixOS. NixOS happens to be the most complete expression of it, but the underlying model is useful to me across platforms.

I love NixOS because it fits especially well with the way I work in the current LLM coding era.

Tools are changing very quickly. Coding agents often need very specific versions of utilities, compilers and runtimes. They need to install something, use it, throw it away, try another version and keep going without turning my PC into a garbage dump of conflicting state. Nix fits that model naturally. If I tell a coding agent that I use Nix, it is usually clever enough to reach for nix shell or nix develop to bring the needed tool into an isolated environment and execute it there. That is especially handy because Nix treats tooling as a declared input instead of an accidental side effect on the system.

A concrete example: I recently built a voice-to-text agent in Rust5. I did not have the Rust toolchain installed on my system. I simply told the coding agent that I use Nix, and it figured out how to pull in the entire Rust toolchain through Nix, compile the project inside an isolated shell and produce a working binary. My base system was never touched. No ~/.cargo, no ~/.rustup, no mutated PATH entries left behind. Without Nix, the agent would have reached for curl | sh to install rustup, quietly mutated my environment and left my system slightly different forever. With Nix, none of that happened.

This pattern generalizes. Every time an agent needs Python 3.11 vs 3.12, a specific version of ffmpeg, an obscure CLI tool or a particular compiler, Nix gives it a clean and reversible way to get exactly what it needs. The agent does not have to guess whether a tool is already installed or in the wrong version. It just declares what it needs and Nix takes care of the rest in a sandboxed way.

The other thing I appreciate is that Nix turns an agent's experiment into something you can actually commit and reproduce. Once the agent has a working setup, you can capture the exact dependencies in a flake.nix and run nix flake check to verify it builds cleanly from scratch. That transforms an ad hoc agent session into a reproducible, verifiable artifact. That is a much stronger foundation for delivering something that works reliably in production than hoping the environment happens to be in the right shape on the next machine.

I love NixOS because I like what Nix gives me in deployment too. I have never been a big fan of Docker as the final answer to the "works on my machine" problem. It solved important problems for the industry, no doubt about that, but I always found the overall model less satisfying than a truly deterministic one. Nix gives me a much better story. I can use dockerTools.buildLayeredImage to build smaller Docker images in a deterministic and layered approach. If I can build it on one computer with the proper configuration, I can build the same artifact on another one as long as Nix supports the architecture, which in my experience has been very reliable.

That coherence is one of the things I value most about NixOS. The same underlying model helps me with my laptop, my shell, my project dependencies, my CI pipeline and my deployment artifact. It is one way of thinking about software instead of a loose collection of unrelated tools and habits.

So when I say I love NixOS, what I really mean is that I love what it represents. I love a system that is declarative, reproducible, reversible and stable. I love being able to experiment without fear and upgrade without drama. I love that it helps me focus on building and experimenting with fast-moving tools, including LLM coding agents, without worrying about messing up my system in the process.

I love NixOS because it is the most complete everyday expression of what I think software systems should be.

Footnotes:

1

If you are new to Nix, I wrote a more practical getting-started guide here: Nix: Better way for fun and profit.

2

By unstable channel I mean the official `nixos-unstable` or `nixpkgs-unstable` channels. See Channel branches and channels.nixos.org.

3

HP EliteBook X G1a 14 inch Notebook with 64 GiB RAM and AMD Ryzen AI 9 HX PRO 375.

4

For example, nix develop drops you into an interactive shell environment that is very close to what Nix would use to build the current package or project.

5

A voice-to-text agent I built in Rust that replaced Whisper and Willow Voice in my personal workflow. I wrote it first for macOS and then ported it to Linux. I have been using it as a daily driver for a couple of months now. I am considering open sourcing it or releasing it as a standalone app.

Tags: nix ai
15 Apr 2025

Nix: Better way for fun and profit

Nix is started in 2003 as a research project aimed to solve the problem of reliable software deployment. The PhD thesis titled The Purely Functional Software Deployment Model proposed a novel way of building software where the final artifact is purely dependent on the inputs to the build system, which is a pure function in a mathematical sense. Regardless of where you are in your nix journey, I can't recommend this paper (thesis) enough. It is very approachable and worth a read so you learn from first principle of what, why and how about Nix.

Nix is a software build and management system that can replace traditional package managers, build environments and configuration tools. Due to the inherent complexity of the problem domain nix is designed to solve and its long history, it has pretty steep learning curve but not unsurmountable. One of the common point of confusions is how the term `Nix` is used in documentations, tutorials and blogosphere. So let me clarify few terminologies that often gets overloaded.

After few false starts and restarts, below are what I believe to be better ways for getting started, using nix for fun and profit.

Installation

I have a following bash script to install a specific version so I can have control over which version to install, what features enable and disable.

#!/usr/bin/env bash
set -Eeuo pipefail

VERSION='2.28.1' # replace it with the latest version
URL="https://releases.nixos.org/nix/nix-${VERSION}/install"
MY_CONF="$HOME/.dotfiles/nix/nix.conf"
sh <(curl --location "${URL}") \
     --daemon \
     --no-channel-add \
     --nix-extra-conf-file ${MY_CONF}
# conf file has this content
experimental-features = nix-command flakes

The `–no-channel-add` and the extra conf file needs some explanation. Nix called a remote url a channel that gets automatically installed, where nix uses to retrieve package definitions (Nix DSL) to manage packages. It introduces a state, which is currently installed channel url that is outside of Nix DSL, thus defeating the purpose of reproducibility. It is considered legacy feature and not needed by flakes, an experimental feature already widely adopted by the community. So I highly recommend enabling flakes and additional commands to interact with it.

Using for fun and sanity

Every project depends on existing software that is beyond your control. Nix DSL enables you to declaratively specify your projects dependencies, a repo or a tar-ball down to the file digest of its content, which is what gives nix superpowers of being a deterministic and reproducible package manager. This means that if your inputs stays the same, nix guarantees that it produces the exact same output regardless of when and where. Below is a flake that pulls in latest version of Clojure into your project.

{
  # optional attribute
  description = "My awesome Clojure/ClojureScript project";

  # required attribute
  inputs = {
    # nix dsl fns useful for writing flakes
    flake-utils.url = "github:numtide/flake-utils/v1.0.0";
    # Pins state of the packages to a specific commit sha
    pinnedPkgs.url = "github:NixOS/nixpkgs/c46290747b2aaf090f48a478270feb858837bf11";
  };

  # required attribute
  outputs = { self, flake-utils, pinnedPkgs }@inputs :
  flake-utils.lib.eachDefaultSystem (system:
  let pinnedSysPkgs = inputs.pinnedPkgs.legacyPackages.${system};
  in
  {
    devShells.default = pinnedSysPkgs.mkShell {
      packages = [
        pinnedSysPkgs.clojure
      ];

      # commands to run in the development interactive shell
      shellHook = ''
        echo To get Clojure REPL, Run:
        echo clojure
        echo To get ClojureScript REPL, Run:
        echo clj -Sdeps \'{:deps {org.clojure/clojurescript {:mvn/version "1.11.132"}}}\' -M -m cljs.main --repl
      '';
    };
    packages = {
      docker = pinnedSysPkgs.dockerTools.buildLayeredImage {
        name = "My awesome Clj docker image built by nix";
        tag = "latest";
        contents = [pinnedSysPkgs.clojure];
      };
    };
  });
}

Do not worry too much about not understanding above nix dsl code. The most important thing to know is that it is nix dsl referred to as a flake that specifies its inputs and outputs declaratively. Save above code as `flake.nix`, which is a convention, then run `nix develop` to get an interactive shell with Clojure in your path. Nix can do way more than this. However, I recommend you just start with solving project dependencies problem. Above flake gives you following benefits:

  • Ability to pin the exact versions of your project dependencies.
  • Cross platform development environment that works both in MacOS and various flavors of Linux.
  • Determinate and reproducible development environment that eliminates "it works on my machine" tooling issues.

One important thing to notice here is the way I chose to reference the url inputs of the flake. I deliberately used tags or commit sha to prevent the state of the urls (thus the state of the nix DSL) change under me, which defeats the purpose of having a determinate and reproducible way to get a development environment. I have following bash script that prints available tags and corresponding commit hash:

 git_tag_sha () {
   repo="$1"
   echo "********************************************************"
   echo "Available release and commit sha for pinning are:"
   echo "********************************************************"
   printf "\033[1m%-12s %s\033[0m\n" "release" "commit sha"
   curl -s https://github.com/$repo/tags | grep -oP 'href="\K[^"]*(releases/tag|nixpkgs/commit)[^"]*' | awk -F '/' 'NR%2{tag=$NF; next} {printf "%-12s %s\n", tag, $NF}'
   echo
   echo "****************************************************************************"
   echo "Please replace the commit sha of following line to pin pkgs to a commit sha: "
   echo "pinnedPkgs.url = github:$repo/<commit>"
   echo "****************************************************************************"
   echo
}
# You can run it like this:
 git_tag_sha "NixOS/nixpkgs"

Profiting in CI/CD and production

This is probably one of the most frictionless and rewarding outcome of using nix. Nix is designed to solve the problem of software deployment after all but the wholesale adoption in production might prove to be too much for the final gain. To spare yourself countless hours of frustration, I highly recommend you start with using it to build docker image if you happened to use docker and Kubernetes. Nix has superb built-in support for making the smallest possible docker image otherwise impossible. Above flake already includes `docker` image as one of its packages output. Here is how you build and load the docker image:

nix build .#docker # the image will be in ./result
docker load < ./result # to get it ready to be deployed

It is a declarative way (using the power of Nix DSL compared to using series commands in YAML file) to deterministically reproduce layered Docker image that saves time and money in your DevOps journey. Have fun and enjoy!

Tags: nix
Other posts