
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.
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.
# 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
In a project root, create .envrc:
# .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:
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.
.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:
# .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.
Different projects want different models. Drop a tiny helper in ~/.config/direnv/lib/use_model.sh:
# ~/.config/direnv/lib/use_model.sh
use_model() {
export ANTHROPIC_MODEL="$1"
echo "🔁 Anthropic model → $1"
}Now in ~/work/client-a/.envrc:
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:
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.
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.code . from a direnv-loaded terminal, or add "terminal.integrated.inheritEnv": true to settings.json.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.direnv allow anything. Just use real environment variables or a .env file your CI loads directly.allow step, and you want explicit, auditable env injection via systemd, Docker, or your orchestrator./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.