← Back to Payloads
Tutorial2026-07-07

Stop exporting API keys in every shell: set up direnv in 5 minutes

Per-project OpenAI, Anthropic, and Ollama keys without symlinks, dotfile hacks, or 'which terminal am I in?' confusion. Just walk into a directory and the right keys are live.
Quick Access
Install command
$ mrt install tutorial
Browse related skills
Stop exporting API keys in every shell: set up direnv in 5 minutes

Stop exporting API keys in every shell: set up direnv in 5 minutes

I have five OpenAI keys, two Anthropic orgs, and a local Ollama on a random port — and until last month I was copy-pasting export lines into ~/.zshrc like a caveman. Worse: I was burning real Anthropic credits on my side project with keys meant for a client's bill. direnv fixes this in 5 minutes and it's the closest thing to magic I get from a shell tool.

The 60-second recipe

Install it, allow your shell hook once, drop a .envrc in a project root, and direnv allow it. Now cd in, cd out, and your env vars follow you.

bash
# macOS
brew install direnv
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
exec $SHELL
# Ubuntu/Debian
sudo apt install direnv && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && exec $SHELL

The 5-minute version (the version you actually want)

In a project root, create .envrc:

bash
# .envrc
export OPENAI_API_KEY="sk-proj-..."
export ANTHROPIC_API_KEY="sk-ant-..."
export OLLAMA_HOST="http://localhost:11434"
# Optional: per-project model defaults
export LLM_MODEL="claude-opus-4-7"
export LOG_LEVEL="debug"

Then:

bash
direnv allow .

That's it. cd .. and the keys disappear. cd project/ and they come back. Open a new tab? Same thing. No more source ./setenv.sh incantations.

Use a .env file instead (the grown-up version)

If you'd rather not commit a .envrc with secrets, keep them in a gitignored .env and have .envrc source it:

bash
# .envrc — safe to commit
dotenv_if_exists .env
# .env — gitignored
OPENAI_API_KEY=sk-proj-real-key
ANTHROPIC_API_KEY=sk-ant-real-key

Add .env to .gitignore (direnv's own install docs walk you through it). This is the pattern I now use for every side project.

Per-directory model switching (the actually useful trick)

Different projects want different models. Drop a tiny helper in ~/.config/direnv/lib/use_model.sh:

bash
# ~/.config/direnv/lib/use_model.sh
use_model() {
  export ANTHROPIC_MODEL="$1"
  echo "🔁 Anthropic model → $1"
}

Now in ~/work/client-a/.envrc:

bash
source ~/.config/direnv/lib/use_model.sh
use_model claude-opus-4-7
export ANTHROPIC_API_KEY="sk-ant-client-a-..."

And ~/work/side-project/.envrc:

bash
source ~/.config/direnv/lib/use_model.sh
use_model claude-sonnet-5
export ANTHROPIC_API_KEY="sk-ant-personal-..."

Walk into client-a and your CLI tools, Claude Code, and curl tests all see claude-opus-4-7. Walk into side-project and they see claude-sonnet-5. No restart, no aliases, no re-sourcing.

Gotchas

  • **You must run direnv allow . once per machine, per .envrc file.** This is a security feature — it stops a malicious repo from silently exporting its own keys the moment you cd in. Commit a .envrc.example for teammates; each one runs direnv allow on their first pull.
  • VS Code's integrated terminal inherits direnv fine, but if you launch the editor from a desktop launcher (Spotlight, dock, etc.), the env may be empty. Fix: run code . from a direnv-loaded terminal, or add "terminal.integrated.inheritEnv": true to settings.json.
  • Docker / Make targets don't see direnv vars by default. make invokes a child shell that does see them, but docker run does not. Use docker run --env-file <(direnv exec . env | grep -v '^#') if you need them inside the container.
  • **.envrc files must be world-readable** and live in a directory you trust. direnv will refuse to load anything owned by a different user.

When NOT to do this

  • CI/CD runners. They don't have an interactive shell to direnv allow anything. Just use real environment variables or a .env file your CI loads directly.
  • Production servers. Same reason — no allow step, and you want explicit, auditable env injection via systemd, Docker, or your orchestrator.
  • **Quick one-off scripts in /tmp.** The friction of allow isn't worth it. Just prefix the command: OPENAI_API_KEY=sk-... ./oneoff.py.

Five minutes of setup. I haven't typed export in a terminal in three weeks. That's the bar.

Related Dispatches