Handling environment variables in Emacs on OSX and Linux

July 26, 2026 | emacs

I read the post A Couple Of Tips For Emacs on macOS, where Irreal covers JTRs Emacs configuration series, and they raise two pain points on OSX. One of the tips is how to ensure environment variables are properly set inside Emacs, by using exec-path-from-shell. So I thought I would share how I deal with it.

On OSX, Emacs is launched from Finder or from the Dock rather than a shell, so it doesn’t inherit your shell’s environment the way terminal Emacs would. The same can happen with Emacs on Linux if you’re not spawning the process from a shell, or if your application launcher isn’t configured to invoke from the shell.

One of the issues I had with exec-path-from-shell is that it was still slow even after I configured my shell to expose the environment variables in a non-interactive shell. I also didn’t want Emacs to invoke a subprocess at startup. So I did something similar to how Doom Emacs does: I now generate a file with all the environment variables I want, and make Emacs read that file and load these variables.

In my early-init.el, I added this code:

;; Load shell env snapshot generated by emacs-refresh-env.
;; Mirrors Doom's approach: snapshot is written once by a script running
;; `zsh -ic env`, so no subprocess is needed at startup.
(let ((env-file (expand-file-name "env" user-emacs-directory)))
  (when (file-readable-p env-file)
    (dolist (line (split-string (with-temp-buffer
                                  (insert-file-contents env-file)
                                  (buffer-string))
                    "\n" t))
      (when (string-match "^\\([^=\n]+\\)=\\(.*\\)$" line)
        (setenv (match-string 1 line) (match-string 2 line))))
    (when-let* ((path (getenv "PATH")))
      (setq exec-path (append (split-string path ":") (list exec-directory))))))

Then I created a script, emacs-refresh-env, which copies the environment variables from my shell and puts them into the env file, which Emacs reads. The script ignores some environment variables and filters out Tmux/Alacritty-generated noise.

#!/usr/bin/env zsh
# emacs-refresh-env: regenerate the Emacs shell env snapshot.
# Run after changing .zshenv or .zshrc.
set -euo pipefail

emacs_d="$HOME/.emacs.d"
out="${emacs_d}/env"

# Vars to omit from the Emacs env snapshot (shell bookkeeping, etc.).
exclude=(
  _
  SHLVL
  PWD
  OLDPWD
  DIRENV_DIFF
  DIRENV_DIR
  DIRENV_FILE
  DIRENV_WATCHES
  EDITOR
  VISUAL
  LS_COLORS
  ALACRITTY_LOG
)

pattern="^(${(j:|:)exclude})="
# Drop excluded vars and tmux/terminal garbage (e.g. leaked \033Ptmux;…ALACRITTY_LOG=…).
zsh -ic env </dev/null |
  grep -E '^[A-Za-z_][A-Za-z0-9_]*=' |
  grep -Ev "$pattern" |
  sort >"$out"
echo "Wrote $(wc -l <"$out" | tr -d ' ') vars to $out"

Once the script executes, it creates the env file inside my user-emacs-directory and early-init.el picks it up when starting Emacs.

The benefit of this is that there is no external dependency on exec-path-from-shell anymore, and it’s fast, but the trade-off is you need to always make sure you generate the env file in case your environment variables change.

I have an update-all script which updates my machine, along with some other dependencies, so I hooked emacs-refresh-env into it, ensuring it runs periodically and picks up new environment variables.


No notes link to this note

Go to a random page