{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/misc/direnv/",
"description": "Automate environment variables per directory with direnv. Load .envrc files on entry, unload on exit. Integrate with Python venv and uv workflow.",
"path": "/misc/direnv/",
"publishedAt": "2024-10-02T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"TIL",
"Shell",
"Unix",
"CLI"
],
"textContent": "I'm not a big fan of shims - code that messes with commands in the shell or prompt. That's\nwhy, aside from occasional dabbling, I tend to eschew tools like asdf or pyenv and just\nuse apt or brew for installs, depending on the OS.\n\nThen recently, I saw [Hynek extolling direnv]:\n\n> If you're old-school like me, my .envrc looks like this:\n>\n> \n>\n> The sync ensures there's always a .venv, so no memory-baking required.\n\nAnd [Brandur doing the same]:\n\n> This is embarrassing, but after using direnv for 10+ years, I only discovered the\n> source_env directive yesterday.\n>\n> Game changer. I used it to improve our project's dev configuration ergonomics so new\n> environment variables are easily distributed via Git.\n\nSo I got curious and wanted to try the tool to see if it fits into my workflow, or if I'll\nquickly abandon it when something goes wrong.\n\nWhen I first visited their [landing page], I was a bit confused by the tagline:\n\n> direnv – unclutter your .profile\n\nBut I don't have anything custom in my .profile, or more specifically, my .zprofile.\nHere's what's in it currently:\n\nThen I realized that .profile is used here as a general term for various configuration\nfiles like .profile, .rc, and .env. I have quite a bit set up in both my /.zshrc\nand /.zshenv - a mix of global and project-specific commands and environment variables.\n\nTo explain: .profile files (like .profile or .bash_profile) are used by login shells,\nwhich are started when you log into a system, such as through SSH or a terminal login. In\ncontrast, files like .bashrc or .zshrc are for interactive shells, meaning they run when\nyou open a new terminal window or tab. For Zsh, .zshenv is sourced by all types of\nshells - both login and interactive - making it useful for global environment settings.\n\nWhat problem it solves\n\nDirenv solves the hassle of managing environment variables across different projects by\nautomatically loading them when you enter a directory and unloading them when you leave. It\nkeeps your global environment clean and avoids cluttering up your shell configuration files.\n\nIt checks for an .envrc (or .env) file in the current or parent directories before each\nprompt. If found and authorized, it loads the file into a bash sub-shell and applies the\nenvironment variables to the current shell.\n\nIt supports hooks for common shells like Bash, Zsh, Tcsh, and Fish, allowing you to manage\nproject-specific environment variables without cluttering your /.profile. Since it's a\nfast, single static executable, direnv runs seamlessly and is language-agnostic, meaning you\ncan easily use it alongside tools like rbenv, pyenv, and phpenv.\n\nYou might argue that source .env works just fine, but it's an extra step to remember.\nAlso, being able to communicate the project-specific environment commands and variables, and\nhaving them sourced automatically, is a nice bonus.\n\nWhy .envrc file and not just a plain .env file\n\nThis was the first question that came to my mind: why not just use a .env file? Why\nintroduce another configuration file? Grokking the docs clarified things.\n\nThe .envrc file is treated like a shell script, where you can also list arbitrary shell\ncommands that you want to be executed when you enter a project directory. You can't do that\nwith a plain .env file. However, direnv does support .env files too.\n\nIt's such a simple idea that opens up many possibilities.\n\nHow I use it\n\nHere are a few things I'm using it for:\n\n- Automatically loading environment variables from a .env file.\n- Loading different sets of values for the same environment keys, e.g., local vs. staging\n values.\n- Activating the virtual environment when I enter the directory of a Python project.\n\nLet's say you want to load your environment variables automatically when you cd into a\ndirectory and have them removed from the shell environment when you leave it. Suppose the\nproject directory looks like this:\n\nThe .env file contains environment variables for local development:\n\nAnd the .env.staging file contains the variables for staging:\n\nThe .envrc file can have just one command to load the default .env file:\n\nNow, from the svc directory, you'll need to allow direnv to load the environment variables\ninto the current shell:\n\nThis prints:\n\nYou can now print the values of the environment variables like this:\n\nThis returns:\n\nIf you want to load different variables depending on the environment, you can add the\nfollowing shell script to the .envrc file:\n\nThe script loads the .env.staging file if the value of $ENVIRONMENT is staging;\notherwise, it loads the default .env file. From the svc root, run:\n\nThis will still load the variables from .env. To load variables from .env.staging, run:\n\nThis time, printing the variables returns the staging values:\n\nOh, and when you leave the directory, the environment variables will be automatically\nunloaded from your working shell.\n\nYou can do a lot more with the idea, but going overboard with environment variables can be\nrisky. You don't want to accidentally load something into the environment you didn't intend\nto. Keeping it simple with sane defaults is the way to go.\n\nLike Hynek, I've adopted [uv] in my Python workflow, and now my default .envrc has these\ntwo commands:\n\nThe first command updates the project's environment without changing the uv.lock file, and\nthe second ensures I never need to remember to activate the virtual environment before\nrunning commands. Now, when I cd into a Python project and run:\n\nIt shows that the local .venv is active:\n\nNo more worrying about mucking up my global Python installation while running some commands.\n\nAnother neat directive is source_up, which lets you inherit environment variables from the\nparent directory. Normally, when you move into a child directory, direnv unloads the parent\ndirectory's environment variables. But with the source_up directive in your .envrc,\nit'll keep those variables around in the child directory.\n\nThen there's the source_env directive, which lets you pull one .envrc file into another.\nSo, if you've got some common, non-secret variables in an .envrc.local file, you can\neasily reuse them in your .envrc.\n\nHere's an example .envrc.local file:\n\nYou can import .env.local into the .envrc file like this:\n\nI haven't used source_env much yet, but I love the possibilities it unlocks.\n\nThe biggest reason I've adopted it everywhere is that it lets me share my shell environment\nvariables and the magic commands without having anything stashed away in my /.zshrc or\n~/.zshenv, so there's no need for out-of-band communication.\n\n\n\n\n\n[hynek extolling direnv]:\n https://x.com/hynek/status/1838076629249044533\n\n[brandur doing the same]:\n https://x.com/brandur/status/1837104038854164645\n\n[landing page]:\n https://direnv.net/\n\n[uv]:\n https://github.com/astral-sh/uv",
"title": "Discovering direnv"
}