envoke

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT

README

envoke

Documentation

envoke runs shell scripts when you cd into or out of a directory.

It's a spiritual rewrite of ondir in Go: same idea (per-directory enter/leave hooks matched by path), rewritten to fix a decade of unresolved bugs and to reach every major shell and OS with a single static binary.

Status: early development. The core matching engine, shell hooks for bash/zsh/fish/tcsh/PowerShell, the envoke allow trust mechanism, and envoke debug dry-run diagnostics all exist and are tested — a cd into a trusted, matching directory really does run its enter block in your shell today. bash, zsh, and tcsh have been verified end to end against real interpreters; fish and PowerShell are implemented against documented behavior but not yet run against real fish/pwsh interpreters (neither is installed in the environment this was built in). Still missing: packaging/releases. This README describes the target shape of v1. See CLAUDE.md for the MVP scope order.

Why not just use ondir / direnv?

  • ondir nails the enter/leave-with-regex-matching model but is effectively unmaintained (last push 2023, maintainer considers it feature-complete), uses POSIX regex vulnerable to catastrophic backtracking (hangs), and has several long-standing correctness bugs (see Design notes).
  • direnv is mature and trusted, but it's centered on a single .envrc per directory with load/unload semantics — it doesn't do regex-based matching across a directory tree, and isn't aimed at the "run an arbitrary script on enter/leave of any matching path" use case.

envoke targets the ondir model — path-pattern-driven hooks, not one-file-per-directory — with direnv-grade reliability and trust.

Core concept

A config file (~/.envokerc by default) declares blocks:

enter ~/Projects/([^/]+)
    source venv/bin/activate

leave ~/Projects/([^/]+)
    deactivate
  • enter <path-pattern>: runs when you cd into a directory matching the pattern (matched with Go's regexp/RE2 — linear time, no catastrophic backtracking).
  • leave <path-pattern>: runs when you cd out of a directory matching the pattern.
  • Moving through intermediate directories still triggers their rules, even if you don't stop there.
  • Enter and leave are independent, explicit blocks — envoke does not snapshot state on enter and auto-restore it on leave. If entering exports a variable or activates a venv, the matching leave block is responsible for explicitly unwinding it. This keeps behavior predictable and scriptable instead of relying on magic state-diffing.

Use cases

  • Python (or any) virtualenv activation — enter sources venv/bin/activate, leave runs deactivate.
  • Per-directory umask — enter tightens umask for a sensitive tree, leave restores the default explicitly.
  • Project-scoped environment variables — enter exports API keys/endpoints/feature flags for a project, leave unsets them.
  • Cloud/k8s context switching — enter runs kubectl config use-context ... / sets AWS_PROFILE / activates a gcloud configuration for a directory tree; leave switches back explicitly. Reliable because path matching is segment-based, not raw-prefix (ondir's bug #5: /home/foo no longer falsely matches /home/foobar).
  • Capture groups exposed to scripts — enter ~/Projects/([^/]+) can expose the matched project name to the script (e.g. via ENVOKE_MATCH_1), so one generic block handles many project directories instead of duplicating config per project.
  • On-the-fly shell function generation — enter generates/sources a functions.sh tailored to the current project (e.g. from Docker Compose labels), leave cleans it up. Per-rule enter/leave state tracking avoids regenerating on every deeper cd within an already-active tree.
  • Dev toolchain version switching — enter swaps Node/Ruby/Go versions for a project tree without depending on each tool's own per-directory convention; one mechanism for all of them.

Trust model

Any config that runs arbitrary shell code on cd needs an opt-in step before it executes for the first time, direnv-style (direnv allow). ondir has no such mechanism — any ~/.ondirrc runs unconditionally. envoke requires an explicit envoke allow (or equivalent) before a new or changed config block is executed.

Diagnostics

envoke debug <from> <to> prints which enter/leave blocks would fire for a given directory transition, without running them — for developing a config without surprises.

Design notes

Bugs in ondir that motivate specific design choices here:

ondir issue envoke fix
Catastrophic backtracking hangs (glibc POSIX regex) Go's regexp (RE2), linear-time matching guaranteed
Basename-prefix false positives (/home/foo matches /home/foobar) Path-segment matching, not raw string prefix
No ~ expansion in config paths Explicit ~/env-var expansion
Capture groups not exposed to scripts Matched path and capture groups exposed as env vars
No ONDIRRC var / no XDG support ENVOKERC env var + $XDG_CONFIG_HOME/envoke/config
Hand-maintained shell scripts per shell (bash/zsh/tcsh/fish) Single binary generates hooks: envoke shell-init bash|zsh|fish|tcsh|powershell
zsh integration overrides cd directly Proper chpwd_functions (zsh), --on-variable PWD (fish), cwdcmd (tcsh), wrapped prompt (PowerShell)
No config trust/opt-in envoke allow before executing new/changed config

Installation

Not yet published. Planned distribution, once v1 is buildable:

  • GitHub Releases — prebuilt static binaries for macOS/Linux/Windows (amd64 + arm64), built with goreleaser.
  • Homebrew (macOS/Linux) — brew install neirda24/tap/envoke.
  • Scoop (Windows) — scoop bucket add neirda24 https://github.com/Neirda24/scoop-bucket && scoop install envoke.
  • Linux packages — .deb/.rpm built via nfpm as part of the goreleaser pipeline.
  • Universal install script — curl -sSL https://.../install.sh | sh, detects OS/arch and pulls the matching release binary.
  • Go toolchain — go install github.com/Neirda24/envoke@latest for Go users who prefer building from source.

winget (Windows) is a candidate follow-up once the project has enough release history to meet its submission requirements.

Shell integration

Add to your shell rc file:

# bash/zsh
eval "$(envoke shell-init bash)"   # or zsh
# fish
envoke shell-init fish | source
# tcsh — `eval "$(...)"`-style substitution splits multi-line output on
# newlines in tcsh, so this pipes into source instead.
envoke shell-init tcsh | source /dev/stdin
# PowerShell — Out-String joins the (possibly multi-line) output into one
# string before Invoke-Expression evaluates it.
& envoke shell-init powershell | Out-String | Invoke-Expression

License

MIT

Directories

Path Synopsis
cmd
envoke command
Command envoke runs shell scripts when you cd into or out of a directory.
Command envoke runs shell scripts when you cd into or out of a directory.
internal
config
Package config parses envoke config files into a set of enter/leave blocks, each pairing a path pattern with a shell script body.
Package config parses envoke config files into a set of enter/leave blocks, each pairing a path pattern with a shell script body.
envoke
Package envoke wires the config, matcher and executor packages together into the core enter/leave loop.
Package envoke wires the config, matcher and executor packages together into the core enter/leave loop.
executor
Package executor runs a matched enter/leave block's script.
Package executor runs a matched enter/leave block's script.
matcher
Package matcher resolves which enter/leave config blocks fire for a directory change, including ondir-style traverse behavior: jumping straight from /a to /a/x/y/z still fires the rules for the intermediate directories /a/x and /a/x/y, not just the destination.
Package matcher resolves which enter/leave config blocks fire for a directory change, including ondir-style traverse behavior: jumping straight from /a to /a/x/y/z still fires the rules for the intermediate directories /a/x and /a/x/y, not just the destination.
shellinit
Package shellinit generates the shell hook code printed by `envoke shell-init <shell>`, meant to be eval'd/sourced from the user's shell rc file (e.g.
Package shellinit generates the shell hook code printed by `envoke shell-init <shell>`, meant to be eval'd/sourced from the user's shell rc file (e.g.
trust
Package trust implements envoke's config trust store: a config file must be explicitly approved with Allow before shell-hook will act on it, and any change to the file's content — even whitespace — revokes that trust until Allow runs again.
Package trust implements envoke's config trust store: a config file must be explicitly approved with Allow before shell-hook will act on it, and any change to the file's content — even whitespace — revokes that trust until Allow runs again.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL