envoke vs. direnv¶
direnv is the tool most people already have when they find envoke, so this page is the honest point-by-point: what each one is shaped for, where they overlap, and when it's worth swapping — or running both.
The short version
direnv manages environment variables, declared by a file inside each directory, and restores them for you when you leave.
envoke runs arbitrary shell scripts on enter and leave, declared by path pattern in one central config, with nothing inside the directories themselves.
Neither replaces the other outright. If your rules belong to a project and should be committed with it, direnv fits better. If your rules belong to you — spanning trees of repos you don't own, or doing things that aren't environment variables — that's what envoke is for.
Side by side¶
| direnv | envoke | |
|---|---|---|
| Where rules live | An .envrc in each directory |
One central config (~/.envokerc), rules selected by path pattern |
| How a directory is selected | By the file's own location | RE2 regex matched against whole path segments, capture groups exposed to the script |
| What it can change | Environment variables — exported vars captured from a bash subshell | Anything shell code can do; the hook's output is eval'd by your own shell |
| Leaving a directory | Automatic: the previous environment is restored | Explicit: you write a leave block; nothing is auto-undone |
| Script language | bash, always — even if your shell is fish or PowerShell | Your shell's own syntax |
| Trust step | direnv allow, per .envrc |
envoke allow, once per config file, showing a diff on re-approval |
| Shells | bash, zsh, fish, tcsh, elvish, pwsh, murex (+ export formats) | bash, zsh, fish, tcsh, PowerShell |
| Batteries | Large stdlib: layout python, use nix, PATH_add, dotenv, … |
None — you write plain shell |
| Runtime dependencies | Needs bash available to evaluate .envrc |
One static binary, no cgo, nothing else |
1. The rules don't live in the directory¶
This is the difference that brings most people here. direnv needs a file in the directory it acts on, which is a poor fit when:
- the repository isn't yours, or your team doesn't want an
.envrccommitted; - you'd rather not add another entry to
.gitignorein every checkout; - the rule is really "everything under
~/work/", not "this one project"; - you don't want to run an approval command again for each new clone.
It's a long-standing ask on direnv's own tracker — see direnv#1108 "Allow external dir mappings", direnv#280 "How about having a global envrc that describes all direnvs", and direnv#1311.
envoke starts from the other end: one config you own, patterns that cover whole trees, and nothing dropped inside the directories.
# ~/.envokerc — applies to every project under ~/work, including ones cloned tomorrow
enter ~/work/([^/]+)
export PROJECT_NAME="$ENVOKE_MATCH_1"
export AWS_PROFILE=work
The flip side, stated plainly: a per-project .envrc travels with the
repo. Your teammates get the same environment by cloning, with no setup step.
A central config only ever configures your machine. If sharing the config
with a team is the point, direnv is the better tool.
See Configuration for the full pattern syntax and the
ENVOKE_* variables a matched block receives.
2. It isn't only about environment variables¶
direnv evaluates .envrc in a bash subshell and captures the exported
variables out of it. That's a deliberate, robust design — it's what lets one
implementation serve every shell — but it also means anything that isn't an
environment variable doesn't survive the trip back: shell functions, aliases,
shell options, a sourced activation script that defines functions.
envoke's shell hook doesn't run your script in a subprocess at all. It emits
shell text in your shell's dialect, which your own shell evaluates — so
source, alias, set -o, and shell functions all work as written:
enter ~/Projects/([^/]+)
source "$ENVOKE_DIR/venv/bin/activate"
alias t='go test ./...'
leave ~/Projects/([^/]+)
deactivate
unalias t
The same applies to side effects that live outside the shell entirely —
switching a kubectl context, renaming a tmux window, adding an ssh key,
tightening umask.
3. Leaving is a script you write¶
direnv restores the environment it changed, automatically. That's genuinely less work, and for pure environment variables it's the better default.
envoke deliberately does not do this. There's no state snapshot and no
auto-undo — a leave block runs only what you wrote in it. The cost is
explicit teardown code; the benefit is that teardown can undo things direnv
can't restore, because they were never environment variables:
enter ~/Projects/infra
kubectl config use-context staging
leave ~/Projects/infra
kubectl config use-context default
This is a design principle, not a missing feature — see Design Notes.
4. Matching is a regex over path segments¶
Because rules are central, envoke needs a way to say which directories a
block applies to. Patterns compile to Go's regexp (RE2), so matching is
linear-time no matter how the pattern is written, and they're anchored so they
match whole path segments — ~/Projects/foo never matches
~/Projects/foobar. Capture groups reach the script as ENVOKE_MATCH_1,
ENVOKE_MATCH_2, … so one block can cover a whole family of directories.
Moving straight from /a to /a/x/y/z fires the rules for /a/x and
/a/x/y too, not just the endpoint.
direnv has no equivalent concept, and doesn't need one — the file's location is the match.
5. Both gate execution, differently¶
Neither tool will run code you haven't approved. Both hash the approved content, and both revoke trust the moment it changes.
The differences are ergonomic:
- One approval, not one per directory. envoke's unit of trust is the config file, so adding a rule for a new project means re-approving the one file you already own — not approving each new clone.
- A diff on re-approval. After the first
envoke allow, later approvals show what changed since the version you trusted, instead of re-dumping the whole file for you to re-read. - No cross-directory unloading. In direnv, an untrusted
.envrcin a subdirectory unloads the trusted parent environment as well — see the discussion in direnv#1493. With one central config there's no such interaction.
envoke also warns when the config file is group- or other-writable: hash-based trust protects against silent modification, not against another local user editing the file outright.
Details in Trust Model, and
envoke debug shows exactly which blocks a given move would
fire — without executing anything.
Can I run both?¶
Yes. They hook the shell independently and don't interfere; a common split is
direnv for repositories that ship their own .envrc, envoke for your own
machine-wide rules across trees. The only thing to watch is that if both set
the same variable for the same directory, whichever hook runs last wins — so
keep them responsible for different things.
Moving a project over¶
A typical .envrc:
export DATABASE_URL=postgres://localhost/myapp_dev
export AWS_PROFILE=myapp-dev
PATH_add bin
The equivalent in ~/.envokerc — note that the teardown is now yours to
write, and that $ENVOKE_DIR is the directory that matched (which isn't
necessarily where you ended up, if you jumped several levels deep):
enter ~/Projects/myapp
export DATABASE_URL=postgres://localhost/myapp_dev
export AWS_PROFILE=myapp-dev
export PATH="$ENVOKE_DIR/bin:$PATH"
leave ~/Projects/myapp
unset DATABASE_URL AWS_PROFILE
export PATH="${PATH#"$ENVOKE_DIR/bin:"}"
Then envoke allow to review and approve it — until you do, none of it runs.
What about ondir, mise, and shadowenv?¶
- ondir — the same model envoke
targets:
enter/leaveblocks matched by path, in one central config. It's feature-complete by its own maintainer's account and still works, but hasn't seen a release in years. Design Notes lists the specific points where envoke departs from it. - mise — tool-version manager that also covers
direnv's env use case via a per-project
mise.toml. Same shape as direnv for the purposes of this page: the rules live in the project. - shadowenv — per-directory
.shadowenv.d, an s-expression DSL, environment-focused. Again, a file in the directory.
The recurring theme: everything else in this space puts a file in the directory and manages environment variables. envoke is the option that keeps the rules in one place you control and lets a block run whatever you want.
Try it¶
brew install neirda24/tap/envoke
Then head to Getting Started — hook your shell, write a
first block, envoke allow, and cd. Other install methods (Scoop, .deb,
.rpm, go install, prebuilt binaries) are listed there too.