Home | Blog | Grump
02 Feb 2026

Using Minimal Emacs with Nix

If you're both an Emacs user and a Nix user, then you should probably be using direnv with the use flake directive, and the emacs-direnv package for the best possible experience.

Sadly I cannot do this, because I am far too grumpy. I am trying to live with minimal dependencies, especially in emacs. That means I refuse to run any emacs-lisp unless:

This last bullet typically means I want to have written it myself.

If you know about nix develop, then skip ahead to the lisp for the smallest nix experience I think I can get away with.

But if not…

Wait, first: what the heck is Nix?

Nix is at least three different things depending on who you talk to:

  • A package manager that specializes in minimal, composable, reproducible builds.
  • A functional programming language for configuring that package manager and defining those packages.
  • A package repository containing enormous numbers of packages.
  • An operating system built from those packages, and configured with that language.

For our purposes, we really only need to know that it's a package manager. Some software projects come with a magic file called flake.nix, and that file contains a list of every tool and library you need to work on that project. If you cd into that project directory and type nix develop, then you'll get a shell containing all those tools.

If you also use direnv and have the magic words use flake in your .envrc, then this will happen automatically every time you cd into your project directory.

If you want to know more about this usage of Nix, see the nix-develop chapter of Zero to Nix.

If you want to know more about this usage of direnv, see this blog by determinate systems.1

My Smallest Emacs-Nix Experience

For a vim user, nix develop can be close to ideal. I want to work on ~/my-cool-project so I:

  • cd ~/my-cool-project
  • nix develop
  • vim main.rs …or whatever
  • :make …or whatever

This works because vim likes to live inside the shell. Once we start nix develop, everything else will work with this project's tools.

Emacs, on the other hand prefers to be the shell. An Emacs user is more likely to reach for something like:

This won't work, because async-shell-command spawns a subshell to run nix develop, and we exit that shell immediately and return to Emacs. We don't stick around in the subshell it created, so we don't get to benefit from the environment that nix set up for us in there.

What I want is a tiny little command that transforms my main Emacs environment into something more like the inside of a nix develop shell.

In fact, I want two functions:

(defun gds-nix-flake-load () ... )

(defun gds-nix-unflake () ... )

Loading a Flake

When we call nix develop on the command-line, it does more-or-less three things:

  1. Downloads any stuff2 that the flake.nix says you need, but that you don't have yet.
  2. Compiles anything that needs compiling
  3. Messes with your environment variables, so that everything is made available to you.

I'm skipping a lot of fun stuff in steps (1) and (2), but that's because for this post, I'm really only interested in (3). In fact, I'm only interested in a tiny sub-step of (3).

In fact, during step (3) nix develop can do all sorts of clever language-specific stuff, messing with your LD_LOAD_PATH if you're doing C, or your CLASSPATH if you're doing Java, or your PYTHONPATH or whatever. But for now, I'm only going to concern myself with what nix develop does to my PATH environment variable. If I need more than that in future, I might end up extending this elisp.

So my overly simple strategy for loading a flake is going to look like this:

  • Use nix develop --command to run echo $PATH inside the flake environment
  • Set my actual PATH to whatever nix did.

There are two challenges with using nix develop --command in this way:

  1. If I run nix develop --command "echo $PATH" how do I ensure I get the $PATH from inside the flake, not outside?
  2. What happens if nix decides to download and compile something?

Let's tackle each of these in turn.

Getting the PATH from nix develop

Here's the problem:

$ echo $PATH
/bin:/usr/bin
$ nix develop --command echo $PATH
/bin:/usr/bin
$ 

In this case the PATH expanded before I got into the flake. I could quote the "$PATH":

$ nix develop --command echo '$PATH'
$PATH
$ 

…but that's not helpful. I could quote the whole command:

$ nix develop --command 'echo $PATH'
/tmp/nix-shell.3XxdIU: line 2202: exec: echo $PATH: not found
$ 

…but that's not helping either.

To fix this, we need to explicitly create another shell inside the nix direnv, and have that shell echo its $PATH:

$ nix develop --command sh -c  'echo $PATH'
/nix/store/ykz6g9bnl3kka132wiw355rzk0bibdqn-gettext-0.25.1/bin:
/nix/store/rvp7qlpf5jqvdckjy1afjb6aha6j8dxg-pkg-config-wrapper-0.29.2/bin:
... <snip> ...
$ 

That's better.

What if we have to download and compile something?

The command nix develop --command sh -c 'echo $PATH' works perfectly, so long as the flake has already been evaluated. In that case, nix just drops us directly into the shell and runs our command with no grumbling.

However, if something has recently changed, then nix might need to download and compile some of the tools it's making available to us. In that case we won't just get the $PATH printed out like we wanted, we'll also get a bunch of nix build output:

$  nix develop --command sh -c  'echo $PATH'
copying path '/nix/store/s42v0h3316mmai275hymxymrjj99ls3i-gawk-5.3.2.tar.xz'
    from 'https://cache.nixos.org'...
...<snip>...
copying path '/nix/store/5angnx3qkafkkl9mxsivcy2jvcfynvjh-bootstrap-stage4-stdenv-linux'
    from 'https://cache.nixos.org'...
building '/nix/store/f45hf6x3kl1pigxbf0cglqc9rdbzkqg3-gawk-5.3.2-env.drv'...
/nix/store/4qmrzygp3k835cszdwn8nfhcg5sxdzzz-autoconf-2.72/bin:
/nix/store/wkdiivvldr7961j1lwd64qn87px1nrk3-automake-1.18.1/bin:
...<snip>...
$ 

In an interactive shell, this isn't so obtrusive. Nix detects the terminal-type you're using. If you're using a terminal that supports it, you get a pretty progress bar during the download/build stage, and that progress bar disappears before your final result is printed.

But in a dumb terminal inside emacs – such as any process outputting to a buffer – nix helpfully prints out everything it downloads and builds.

We will want to ignore all that stuff, and just use the results of our echo command at the end. The answer is to add an easily-findable separating line:

$  nix develop --command sh -c  'echo "********gds-nix-flake-load********" ; echo $PATH'
copying path '/nix/store/s42v0h3316mmai275hymxymrjj99ls3i-gawk-5.3.2.tar.xz'
    from 'https://cache.nixos.org'...
...<snip>...
copying path '/nix/store/5angnx3qkafkkl9mxsivcy2jvcfynvjh-bootstrap-stage4-stdenv-linux'
    from 'https://cache.nixos.org'...
building '/nix/store/f45hf6x3kl1pigxbf0cglqc9rdbzkqg3-gawk-5.3.2-env.drv'...
********gds-nix-flake-load********
/nix/store/4qmrzygp3k835cszdwn8nfhcg5sxdzzz-autoconf-2.72/bin:
/nix/store/wkdiivvldr7961j1lwd64qn87px1nrk3-automake-1.18.1/bin:
...<snip>...
$ 

Now it should be a simple matter to delete everything above the separator, and capture everything below it.

Unloading a Flake

I'll have to keep a record of what my PATH was before I started. So long as I do that, I can always re-set my path to that value any time I want.

A Tiny Bit of Elisp

After all that, it really only takes a few short lines to get what I want. You can download gds-nix-tools.el directly or clone with:

git clone https://kindness.city/git/gds-elisp.git

Or you can copy-paste the bits you want from here:

;;; gds-nix-tools.el --- Use nix from emacs -*- lexical-binding: t; -*-

;; Copyright (C) 2026 Gareth Smith

;; Author: Gareth Smith
;; Created: 2 Feb 2026
;; Keywords: tools
;; URL: https://www.kindness.city/blog/2026-02-02-minimal-emacs-and-nix.html

;; This file is not part of GNU Emacs.

;;; Commentary:

;;   A couple of tiny functions for managing my $PATH in a world that
;; contains nix flakes.
;;
;;   From any directory that contains a `flake.nix', you can run
;; `gds-nix-flake-load'.  That function will add the PATH from the
;; flake to your current PATH environment variable.  At any time you
;; can run `gds-nix-unflake'.  That function will re-set the PATH
;; environment variable to its original value from when you first
;; loaded this package.
;;
;;   You can call `gds-nix-flake-load' multiple times without calling
;; `gds-nix-unflake' in between.  This is like calling `nix develop'
;; within a shell from a previous `nix develop' call.
;;
;;   This code only sets your PATH.  It does not attempt to mess with
;; LOAD_PATH or PYTHON_PATH or anything else that your nix flake
;; might manage for you.
;;
;;   If you want smarter management of more environment variables, I
;; recommend `direnv' with the `use flake' directive, and the Emacs
;; direnv package:
;;          https://github.com/wbolster/emacs-direnv

;;; Code:

(defvar gds-nix-default-PATH (getenv "PATH")
  "This is the 'default' or 'original' value of the PATH environment variable.

The one we return to when we're not operating within a nix flake.  You
can set it if you like, but you shouldn't need to.  We grab it from the
environment when the package is loaded.")

(defun gds-nix-flake-load ()
  "Load the PATH env var described by the flake in the current dir.

We use `nix develop --command' to get the PATH we should be
using."
  (interactive)
  (message "Evaluating flake...")
  (let* ((echoheader "echo \"********gds-nix-flake-load********\"")
         (echopath "echo \"${PATH}\"")
         (echocmd (format "%s ; %s" echoheader echopath))
         (proc (start-process
               "eval-flake" "*Flake Path*"
               "nix" "develop" "--command" "sh" "-c" echocmd)))
    (set-process-sentinel proc 'gds-nix-flake-set-path-from-buffer)))

(defun gds-nix-flake-set-path-from-buffer (process event)
  "When EVENT happens to PROCESS, set the current PATH env var.

EVENT must be a successful completion of PROCESS, or we'll error
out.  PROCESS must have printed a desired PATH into its buffer.  This
function is intended for use with `gds-nix-flake-load'."
  (unless (string-equal event "finished\n")
    (error "Failed to get nix-path in current directory: %s" event))

  (with-current-buffer (process-buffer process)
    (gds-nix-clear-buffer-of-all-but-latest-path)
    (let ((desired-path (buffer-string)))
      (setenv "PATH" desired-path)))
  (message "PATH has been set according to flake"))

(defun gds-nix-clear-buffer-of-all-but-latest-path ()
  "Clear extra gumph from this buffer.

Assuming this buffer has recieved the stdout and stderr of a call to
echopath, then everything above the line
'********gds-nix-flake-load********' will be nix build output, and
possibly output from previous calls to `gds-nix-flake-load'.  We don't
want any of that, so we delete it."
  (end-of-buffer)
  (search-backward "********gds-nix-flake-load********")
  (end-of-line)
  (delete-region 1 (point))
  (delete-char 1))

(defun gds-nix-unflake ()
  "Re-set the PATH env var to `gds-nix-default-PATH'."
  (interactive)
  (setenv "PATH" gds-nix-default-PATH))

(provide 'gds-nix-tools)

;;; gds-nix-tools.el ends here

Footnotes:

1

Determinate systems may be either heroes or villains of the Nix world, depending on your outlook. But if you don't like them, then you probably don't like flakes either, and this blog isn't relevant.

2

tools, libraries, compilers, languages, editors…

Tags: emacs programming nix ux

There's no comments mechanism in this blog, but I welcome emails and fedi posts. If you choose to email me, you'll have to remove the .com from the end of my email address by hand.

You can also follow this blog with RSS and find older posts here.