Skip to content
Go back

direnv + asdf vs mise

By SumGuy 8 min read
direnv + asdf vs mise
Contents

You’ve Got the Right Problem, Wrong Tools (Maybe)

You’re bouncing between Python 3.12 and 3.11 across projects. Your Node version jumps. AWS credentials leak into the wrong shell because you forgot which .env file you’re in. Meanwhile, your coworker just… cd’d into a directory and everything worked.

That’s the promise: automatic per-project language versions and environment variables, loaded invisibly when you enter a directory.

Two camps solve this. The OG combo: direnv + asdf. The newer unified play: mise (the rebranded rtx). Both work. Both have trade-offs. Pick the wrong one and you’re debugging PATH issues at 2 AM.

Here’s the real choice: are you chasing simplicity, or do you need maximum flexibility?

The Combo: direnv + asdf

Think of this as the “use two tools, each obsessively good at one job” approach.

asdf manages language versions. Shim-based, supports 600+ plugins (Python, Node, Go, Rust, Ruby, Postgres, Terraform, you name it). You declare versions in .tool-versions files.

direnv watches your .envrc file and loads environment variables when you enter a directory. It’s pure shell scripting, zero magic, total control.

Together: asdf sets your Python to 3.11, direnv loads your AWS credentials and DATABASE_URL.

Setup: direnv + asdf

Install both:

Terminal window
# direnv (Debian/Ubuntu)
sudo apt install direnv
# asdf (macOS / Linux). Since 0.16 asdf is a single Go binary —
# Homebrew is the easy path, or grab the release binary from GitHub.
brew install asdf
# Add asdf shims to your PATH (bash; adjust for zsh/fish)
echo 'export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH"' >> ~/.bashrc
# Add direnv hook to shell config
eval "$(direnv hook bash)" # or zsh, fish

Per-Project: .tool-versions

In your project root:

python 3.11.0
nodejs 20.9.0
golang 1.21.0

asdf reads this, installs the versions (once), and shims them into your PATH. When you cd into the project, python → 3.11, node → 20.9.

Per-Project: .envrc

Same project, same root:

Terminal window
# Load AWS creds for this project
export AWS_PROFILE=prod-db
export AWS_REGION=us-west-2
# Database connection
export DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
# Python venv (optional, but common)
layout python3
# Load .env file if it exists (careful: don't commit secrets)
dotenv

When you cd into the directory, direnv hooks your shell and runs .envrc. Vars are set. Leave the directory, they’re gone.

layout python3 is a direnv built-in that auto-activates a Python venv in .venv/. No extra commands.

How It Feels

Terminal window
$ cd ~/myapp # direnv runs .envrc, loads DATABASE_URL, etc.
direnv: loading ~/myapp/.envrc
direnv: export AWS_PROFILE AWS_REGION DATABASE_URL
$ python --version # asdf shim
Python 3.11.0
$ echo $DATABASE_URL
postgresql://user:pass@localhost:5432/mydb
$ cd .. # leave the project
direnv: unloading
$ python --version # falls back to system Python
Python 3.9.2

The Upside

The Downside


The New Hotness: mise

mise (formerly rtx) is a single tool that does both, language versioning and env var loading. Written in Rust, fast, fewer moving parts.

Instead of .tool-versions + .envrc, you get one file: mise.toml.

Install mise

Terminal window
# Using curl (recommended)
curl https://mise.jdx.dev/install.sh | sh
# Or Homebrew
brew install mise

Add the hook:

Terminal window
eval "$(mise activate bash)" # or zsh, fish

Per-Project: mise.toml

[tools]
python = "3.11.0"
nodejs = "20.9.0"
golang = "1.21.0"
[env]
AWS_PROFILE = "prod-db"
AWS_REGION = "us-west-2"
DATABASE_URL = "postgresql://user:pass@localhost:5432/mydb"
[tasks.dev]
description = "Start dev server"
run = "python -m uvicorn main:app --reload"
[tasks.build]
description = "Build project"
run = "npm run build && go build ./..."

When you cd into the project, mise loads the versions and env vars. No second file needed.

How It Feels

Terminal window
$ cd ~/myapp
mise: installing python 3.11.0
mise: activating nodejs 20.9.0, golang 1.21.0, python 3.11.0
$ python --version
Python 3.11.0
$ echo $DATABASE_URL
postgresql://user:pass@localhost:5432/mydb
$ mise run dev # Built-in task runner
# Starts uvicorn on port 8000

The Upside

The Downside


Head-to-Head: Decision Matrix

Featuredirenv + asdfmise
Setup complexityTwo installs, two hooksOne install, one hook
Config files2 (.tool-versions, .envrc)1 (mise.toml)
Language support600+ plugins (asdf)200+ (growing)
Env var loadingdirenv (.envrc)Built-in
Task runnerNo (use Make/scripts)Yes
StabilityMature, provenSolid, newer
Learning curveModerateLow
SpeedGood (shim overhead)Faster
DebuggingSeparate tools, easier to isolateSingle tool, check mise status
Team onboardingasdf widely knownGrowing, but less common

When to Use Each

Use direnv + asdf if:

Use mise if:


The Real Answer: It Doesn’t Matter (Much)

Honest take: both work. Both are fast enough. Both won’t burn your house down.

The “wrong” choice is nothingmanually managing Python versions and exporting credentials in 15 different projects. That’s the horror show.

direnv + asdf is the safer bet if you’re pessimistic about tool churn. Mise is the friendlier bet if you’re starting fresh and want simplicity.

If you’re already running asdf and direnv? Don’t migrate just for this. You’re fine. Spend that energy elsewhere.

If you’re picking for a new project or greenfield monorepo? Go with mise. Simpler, faster, and the ecosystem is solid enough that you won’t regret it.

And if your team’s split? Pick one, document it in your README, and move on. Consistency beats micro-optimality every time.

Your 2 AM self doesn’t care which tool you used, they just care that the Python version was right and the database URL loaded automatically.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
SSH Bastion & Jump Host Patterns That Don't Hurt

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts