It's been a while since I wrote for the Emacs Blog Carnival but I have missed it. I decided to dip my toes back in when I saw July 2026's theme - Programming.

I started thinking about how my Emacs configuration for development isn't that special and then I realized that the centerpiece of my environment, direnv and Nix, is something that is worth talking about. I have a whole post on setting them up so this will continue with what that post glossed over: How does this work with Emacs?

The idea started off with the fact that I don't want separate configurations for my CLI and Emacs. I want to just configure it once and things will just work in both. Since getting my shell to see my Emacs configuration seems like a waste of time, I chose to let Emacs read what the shell was doing. What I ended up with is my system-wide Emacs reading the same configuration that my shell does, and then by default, Emacs uses the same paths and interpreters. I don't need a .dir-locals in any project folders to set special paths. It just works.

The center of this is the envrc package. It runs direnv when each buffer is opened so you have the right configuration when you need it. I don't think the envrc maintainers sought to support Nix in their package, but because it supports everything direnv does seamlessly, it supports Nix by default.

The envrc configuration in Emacs is easy:

(use-package envrc
  :hook (after-init . envrc-global-mode))

Then I make sure I set up my .envrc and shell.nix files like I did in my old post and then make sure I run direnv allow and… that is it. Whenever I open up a file in that project after that inside Emacs, envrc will setup the environment automatically. It finds the right interpreter, LSP engine, etc. I don't have to do anything.

This is especially powerful for LSP. This is an actual shell.nix file for an old Python project I maintain:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = [
    pkgs.python311
    pkgs.poetry
    pkgs.basedpyright
  ];


}

I install the LSP server – Basedpyright in this case – within the project instead of globally. That way I know that it has the right version of Python and can get its dependencies and then Eglot starts up just fine. When I need to work on this project, I just cd ~/Projects/project-a and then my shortcut for helm-projectile-switch-project and choose the project folder in Emacs.

For completion's sake, here is the .envrc for the same project:

use nix

The magic is that this is all local to the buffers. I can have two projects open at once, each pinned to a different Python, and Emacs just does the right thing in each.

So in project-a it looks like:

;; a buffer under ~/Projects/project-a
M-! which python
/nix/store/j0r98nqnq891c08q2dns661qnz42fz49-python3-3.11.15-env/bin/python

Then I switch to project-b

;; a buffer under ~/Projects/project-b
M-! which python
/nix/store/jn7s7larin2h9yz8fn9g0zkz5asg8i4d-python3-3.13.12-env/bin/python

Same Emacs, same command, two different interpreters because I'm in buffers in two different projects. I never touched exec-path or dropped a .dir-locals.el in either project. Each buffer is running under its own project's .envrc. Of course if I add environment variables to my .envrc file or change that in any way, I have to do another direnv allow in that folder, or in Emacs run envrc-allow in a buffer in that project.

On the other hand, I can change the shell.nix file as much as I want and it will be automatic when I reload the buffer.

Both my shell and Emacs use the same configuration, and I don't even think about it anymore. It just is so seamless and automatic.