Taming Your Dev Chaos with direnv, .env, and .envrc
Ever felt like you’re playing whack-a-mole with environment variables across a dozen projects? One project’s got DEBUG=true, another’s screaming for API_KEY=secret-sauce, and you’re just there manually sourcing shit like it’s 1999? Yeah, we’ve all been there, sweating bullets trying not to mix up prod and dev configs.
Enter direnv — the lazy dev’s best friend that automatically loads and unloads your env vars based on where you cd. It’s like having a personal assistant that whispers “you’re in the wrong project, dumbass” before you fuck up.
What the Hell is direnv?
direnv is basically a shell extension that gives your terminal superpowers. It hooks into bash, zsh, fish, and whatever else you’re using, automatically loading or unloading environment variables as you bounce between directories. No more “oh shit, I forgot to activate the venv” moments. Just cd and boom — your environment’s ready to party.
Getting This Bad Boy Installed
On macOS with Homebrew (because who doesn’t love brew?):
brew install direnv
On Ubuntu/Debian (for you Linux warriors):
apt install direnv
Now hook it into your shell. For zsh (the cool kids’ choice), slap this in your ~/.zshrc:
eval "$(direnv hook zsh)"
Restart your terminal and you’re golden. Pro tip: If you’re still using bash, no judgment, but maybe upgrade your life while you’re at it.
The .envrc File: Your New Bestie
The .envrc file is direnv’s command center — it’s basically a bash script that runs every time you enter the directory. Think of it as your project’s personal hype man. Here’s a basic example:
# .envrc
export DATABASE_URL="postgres://localhost/mydb"
export DEBUG=true
After you create or tweak your .envrc, you gotta approve it:
direnv allow
Yeah, it’s got security vibes — direnv ain’t about to run random code without your say-so. Smart move, prevents you from accidentally nuking your system with some sketchy project’s .envrc.
The .env File and That Sweet dotenv_if_exists Magic
Most apps these days use .env files for their secrets (thanks to that twelve-factor app gospel). It’s just key-value pairs, no frills:
# .env
SECRET_KEY=mysupersecretkey
API_TOKEN=abc123
DATABASE_URL=postgres://user:pass@localhost/db
Direnv can slurp these up automatically with dotenv_if_exists:
# .envrc
dotenv_if_exists
What it does:
- Checks if
.envexists in your current dir - If yes, loads all the vars
- If no, shrugs and moves on (no drama)
Why this slaps:
.envgoes in.gitignore(secrets stay secret, duh).envrcgets committed (tells your team what vars they need)- Everyone on the team can have their own
.envwith different values — no more “why is this broken on my machine?”
Leveling Up Python Projects with layout uv
With uv dropping like a boss as the fastest Python package manager around, I cooked up a custom layout uv function that pairs perfectly with direnv. It’s lit.
Drop this in your ~/.config/direnv/direnvrc (or ~/.direnvrc if you’re old school):
layout_uv() {
[[ $# -gt 0 ]] && shift
if [[ -d ".venv" ]]; then
VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
fi
if [[ -z $VIRTUAL_ENV || ! -d $VIRTUAL_ENV ]]; then
log_status "No virtual environment exists. Executing \`uv venv\` to create one."
uv venv --allow-existing
VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
fi
PATH_add "$VIRTUAL_ENV/bin"
export UV_ACTIVE=1 # or VENV_ACTIVE=1
export VIRTUAL_ENV
}
Then in your project’s .envrc:
# .envrc
dotenv_if_exists
layout uv
What Does This layout uv Sorcery Actually Do?
- Checks for existing venv: If
.venvis already chilling there, it rolls with it - Creates one if needed: No venv? No problem — runs
uv venvand makes one - Activates it: Slaps the venv’s
bininto yourPATH - Sets the flags: Exports
UV_ACTIVE=1andVIRTUAL_ENVfor tools that care
Watch the Magic Happen
$ cd ~/projects/my-python-project
direnv: loading ~/projects/my-python-project/.envrc
direnv: No virtual environment exists. Executing `uv venv` to create one.
Using CPython 3.13.0
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
direnv: export +UV_ACTIVE +VIRTUAL_ENV ~PATH
$ which python
/Users/alex/projects/my-python-project/.venv/bin/python
$ cd ~
direnv: unloading
$ which python
/usr/bin/python
Boom. Automatic venv activation. Your future self will thank you.
Pro Tips to Not Fuck This Up
1. Secrets in .env, Structure in .envrc
# .envrc (commit this shit)
dotenv_if_exists
layout uv
# Required environment variables (document this for your team)
# DATABASE_URL - PostgreSQL connection string
# API_KEY - Your API key from example.com
# .env (keep this out of git, you idiot)
DATABASE_URL=postgres://localhost/mydb
API_KEY=secret123
2. Drop a .env.example Template
Make a .env.example that shows what vars are needed, minus the actual secrets:
# .env.example
DATABASE_URL=postgres://user:password@localhost/dbname
API_KEY=your-api-key-here
DEBUG=false
3. Use direnv allow Like Your Life Depends on It
Only allow .envrc files from projects you trust. This ain’t a free-for-all — malicious code could ruin your day.
4. Team Up with uv’s pyproject.toml
With layout uv and a pyproject.toml, your whole Python setup is declarative AF:
# pyproject.toml
[project]
name = "my-project"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31.0",
"click>=8.1.0",
]
cd into your project and everything’s ready to roll. No more “pip install” nightmares.
Wrapping This Shit Up
Direnv + .env + custom layout uv = dev environment heaven:
- Automatic AF: No more forgetting to activate venvs
- Secure as Fuck: Secrets stay out of git
- Blazing Fast: uv creates venvs in the blink of an eye
- Team-Friendly: Share
.envrc, keep personal.envfiles
Try it out — once you go automatic, you’ll wonder how you survived the manual era. Your productivity will skyrocket, and you’ll have more time for memes.
Future Vibes: mise-en-place
Direnv is solid and battle-tested, but peep this new kid on the block: mise-en-place (say it like “meez”). It’s the dev tool that wants to be your everything.
Mise combines:
- Tool version management (think asdf/nvm/pyenv but better)
- Environment variable handling (direnv’s job)
- Task running (make/just territory)
The big win? One tool to rule them all. No more juggling direnv + asdf + make like a circus act. One mise.toml config file handles it all:
# mise.toml
[tools]
python = "3.13"
node = "20"
[env]
DATABASE_URL = "postgres://localhost/mydb"
[tasks.dev]
run = "python manage.py runserver"
It auto-activates Python venvs, loads .env files, and plays nice with existing .tool-versions from asdf. Built in Rust, so it’s faster than your ex moving on. If you’re drowning in tool sprawl, mise might be your lifesaver. I’ll drop a full migration guide soon — stay woke!
Links and Shit
- direnv docs - The OG
- uv - Python’s speed demon
- Twelve-Factor App - Config wisdom
- mise-en-place - The unified beast