zsh — alex@blog
alex@blog:~$ ./welcome.sh
alex@blog:~$ ls -la
alex@blog:~/posts$ cat direnv-dotenv-envrc.md

Taming Your Dev Chaos with direnv, .env, and .envrc (Plus a Sneak Peek at mise-en-place)

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:

  1. Checks if .env exists in your current dir
  2. If yes, loads all the vars
  3. If no, shrugs and moves on (no drama)

Why this slaps:

  • .env goes in .gitignore (secrets stay secret, duh)
  • .envrc gets committed (tells your team what vars they need)
  • Everyone on the team can have their own .env with 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?

  1. Checks for existing venv: If .venv is already chilling there, it rolls with it
  2. Creates one if needed: No venv? No problem — runs uv venv and makes one
  3. Activates it: Slaps the venv’s bin into your PATH
  4. Sets the flags: Exports UV_ACTIVE=1 and VIRTUAL_ENV for 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 .env files

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

alex@blog:~/posts$ ls -1 | head