TL;DR How to automatically activate Guix shell environments when entering a project directory using direnv.
In a previous post, I showed how to customize Guix shell environments using manifests. The approach works, but requires you to manually run guix shell -m manifest.scm every time you enter a project. That gets old fast.
Enter direnv.
What is direnv?
direnv is a shell extension that loads and unloads environment variables based on the current directory. When you cd into a project with an .envrc file, direnv automatically sets up the environment. When you leave, it cleans up.
For Guix users, this means automatic shell environments per project - no more typing guix shell every time.
Setup
First, install direnv. On Guix, add it to your profile or system packages:
(packages
(cons* direnv
;; your other packages
%base-packages))
Then, hook direnv into your shell. Add the following to your .bashrc:
# direnv (.envrc)
_direnv_hook() {
local previous_exit_status=$?;
trap -- '' SIGINT;
eval "$(direnv export bash)";
trap - SIGINT;
return $previous_exit_status;
};
if [[ ";${PROMPT_COMMAND[*]:-};" != *";_direnv_hook;"* ]]; then
if [[ "$(declare -p PROMPT_COMMAND 2>&1)" == "declare -a"* ]]; then
PROMPT_COMMAND=(_direnv_hook "${PROMPT_COMMAND[@]}")
else
PROMPT_COMMAND="_direnv_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
fi
fi
Restart your shell or run source ~/.bashrc.
Simple Example
For projects with straightforward dependencies, a one-liner .envrc does the trick:
# Guix development environment with node and pnpm
eval "$(guix shell node pnpm --search-paths)"
When you cd into this directory, direnv will prompt you to allow the .envrc:
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.
Run direnv allow once, and you’re set. Every subsequent visit automatically loads the environment.
Complex Example with Manifests
For projects with custom package configurations (like the OpenSSL trick from my previous post), combine direnv with a manifest:
Create a manifest.scm:
(use-modules (guix profiles)
(guix packages)
(guix search-paths)
(gnu packages node)
(gnu packages rust)
(gnu packages commencement)
(gnu packages tls)
(gnu packages databases))
;; Create a custom OpenSSL package that exports OPENSSL_DIR
(define openssl-with-env-dir
(package
(inherit openssl)
(name "openssl")
(native-search-paths
(append (package-native-search-paths openssl)
(list (search-path-specification
(variable "OPENSSL_DIR")
(files '("."))
(file-type 'directory)
(separator #f)))))))
;; Create a custom GCC package that exports CC and LD_LIBRARY_PATH
(define gcc-with-env-cc
(package
(inherit gcc-toolchain)
(name "gcc-toolchain")
(native-search-paths
(append (package-native-search-paths gcc-toolchain)
(list (search-path-specification
(variable "CC")
(files '("bin/gcc"))
(file-type 'regular)
(separator #f))
(search-path-specification
(variable "LD_LIBRARY_PATH")
(files '("lib"))
(file-type 'directory)
(separator ":")))))))
(packages->manifest
(list node
pnpm
rust
(list rust "cargo")
rust-analyzer
gcc-with-env-cc
openssl-with-env-dir
postgresql))
Then reference it in your .envrc:
# Guix development environment
if [[ -d /run/current-system ]]; then
eval "$(guix shell -m manifest.scm --search-paths)"
fi
The /run/current-system check ensures this only runs on Guix systems - useful if you share your project with developers on other distros.
Conclusion
The combination of direnv and Guix shell environments removes the friction from project-specific tooling. You define your dependencies once, and they’re automatically available whenever you work on the project.