manic-systems/cade
{ "createdAt": "2026-05-25T12:28:50Z", "defaultBranch": "main", "description": "an intelligent, cascading environment manager", "fullName": "manic-systems/cade", "homepage": "", "language": "Rust", "name": "cade", "pushedAt": "2026-06-28T01:59:19Z", "stargazersCount": 26, "topics": [ "devshell", "direnv-alternative", "nix", "nix-flake", "nixos" ], "updatedAt": "2026-06-28T20:47:53Z", "url": "https://github.com/manic-systems/cade"}an intelligent, cascading environment manager. similar to direnv with composable layers that stack as you navigate nested project directories and a strong nix focus.
cade walks up the directory tree from your current directory, collecting .cade configuration files and merging their environments in parent-first order. this lets nested projects inherit and extend parent configurations.
features
Section titled “features”- layered environments:
.cadefiles compose from parent to child directories - multiple sources: load environment variables from nix flakes, nix shell files,
.envfiles, or arbitrary commands - multi-shell support: bash, zsh, fish, nushell, elvish, and murex
- lifecycle hooks: run commands before/after environment load and unload
- environment purification: optionally discard the ambient environment for a layer
- permission system: directories must be explicitly allowed before activation
- safe by construction: values are shell-quoted, so secrets or
$(...)in a.envare never executed - direnv compatibility: reads the common declarative subset of
.envrcfiles (use flake,dotenv, …) without executing them
installation
Section titled “installation”with nix (flakes)
Section titled “with nix (flakes)”add cade as an input and use the module for your platform. the module installs cade and wires its hook into your interactive shells for bash/zsh/fish - otherwise see lib.snippets in the flake or programs.cade.shellSnippets in the nixos module.
note that cade’s next branch is the current “bleeding edge”, so use that if you want it.
from 0.1.2 main will effectively be stable/versioned and should only land tags and backports.
{ inputs.cade.url = "github:manic-systems/cade";
# NixOS outputs = { self, nixpkgs, cade, ... }: { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { modules = [ cade.nixosModules.default { programs.cade.enable = true; } ]; }; };}# nix-darwin, identical optionsdarwinConfigurations.mymac = darwin.lib.darwinSystem { modules = [ cade.darwinModules.default { programs.cade.enable = true; } ];};the module wires the hook into interactive bash, zsh, and fish (the shells these systems integrate centrally). toggle per shell:
programs.cade = { enable = true; enableBashIntegration = true; # default enableZshIntegration = true; # default; needs programs.zsh.enable enableFishIntegration = true; # default; needs programs.fish.enable verbosity = "normal"; # null, quiet, normal, vars, trace longRunningWarningMs = 5000; # null, or a positive integer shellGcRootTtlSeconds = 2592000; # null, or a positive integer direnvCompat = false; # true installs the direnv shim for tools};direnvCompat installs a cade-backed direnv on PATH for editors and other
direnv-aware tools (see [direnv compatibility]!(#direnv-compatibility)). it is
not the shell integration path; interactive shells should use cade hook <shell>. the shim collides with a real direnv in environment.systemPackages,
so install only one.
setting verbosity, longRunningWarningMs, or shellGcRootTtlSeconds makes
the module generate a TOML config file and pass it to cade with --config.
alternatively, set programs.cade.configFile to pass your own strict config
path.
for nushell, elvish, or murex, add the hook to your user shell config; see
[Manual setup]!(#manual-setup) below. the ready-made init lines are also exposed
as config.programs.cade.shellSnippets.<shell> (with the module) and as
cade.lib.shellSnippets.<shell>.
with cargo
Section titled “with cargo”cargo install --path .manual setup
Section titled “manual setup”if you’re not using the nixos module or bash/zsh/fish, add the hook to your shell’s startup:
bash (~/.bashrc):
eval "$(cade hook bash)"zsh (~/.zshrc):
eval "$(cade hook zsh)"fish (~/.config/fish/config.fish):
cade hook fish | sourcenushell (config.nu):
cade hook nushell | save -f ~/.cache/cade/hook.nusource ~/.cache/cade/hook.nuelvish (~/.elvish/rc.elv):
eval (cade hook elvish | slurp)murex (~/.murex_profile):
cade hook murex -> sourcefor nix users with declarative configs, you may want to dump the hook at build time and source it. nushell example:
source ${ (pkgs.runCommand "cade.nu" { } ''${lib.getExe (getFlakePkg inputs.cade)} hook nushell >> "$out"'')}commands
Section titled “commands”cade allow # allow cade in the current .cade directorycade disallow # block cade in the current .cade directorycade edit # open .cade in $EDITOR + allow this pathcade status # show activation state, layer chain, permissionscade hook <SHELL> # print the shell hook initialization codecade enter --shell <SHELL> # activate the environment (used by the hook)cade exit --shell <SHELL> # deactivate and restore the previous environmentcade reload --shell <SHELL> # re-evaluate on directory change (called by the hook)cade lease open --kind ide --project "$PWD" # open a non-shell client leasecade --client-id <ID> reload --shell json # attach a lease while exporting envcade lease refresh --client-id <ID> # extend a leasecade lease close --client-id <ID> # close a leasecade --config /nix/store/cade.toml status # strict config file overridecade --verbosity vars status # quiet | normal | vars | tracecade reads an optional TOML config from $XDG_CONFIG_HOME/cade/config.toml
(usually ~/.config/cade/config.toml). --config <file> is strict: that exact
file is read instead of the XDG default, and a missing or invalid file is an
error. this is intended for wrappers and declarative systems.
verbosity = "normal" # quiet | normal | vars | tracelong_running_warning_ms = 5000shell_gc_root_ttl_seconds = 2592000CLI flags override environment variables, which override the config file.
verbosity controls cade’s human-facing diagnostics:
quiet: no lifecycle text or warningsnormal: load/unload/reload text and warnings (default)vars: normal plus variable names set/cleared/restoredtrace: vars plus layer/cache, external command, and hook details
set CADE_VERBOSITY to apply a level to shell hooks without a config file.
external loaders (load flake, load shell, and call) print a warning if
they run for more than 5 seconds. set long_running_warning_ms in the config
or CADE_LONG_RUNNING_WARNING_MS in the environment to adjust that threshold.
shell GC roots and snapshots are retained for 30 days by default. set
shell_gc_root_ttl_seconds in the config or CADE_SHELL_GC_ROOT_TTL_SECONDS
in the environment to adjust that retention. for non-shell clients such as
editors, open a lease and pass --client-id or CADE_CLIENT_ID when calling
cade; the lease keeps associated roots alive until it is closed or expires.
permissions
Section titled “permissions”cade only composes layers from directories you’ve explicitly allowed. there is no implicit trust of ancestors or descendants.
cade allowapproves the current directory and gap-fills up to your nearest already-approved ancestor (the base), stopping there.- activation composes the contiguous run of allowed config directories
from your current location upward, stopping at the first unapproved or empty ancestor.
an untrusted
.cadeabove your base is never loaded, and a malicious.cadedropped above your project can’t auto-run. - typical flow:
cade allowat your project root (the base), thencade allowat a deep sub-project. the layers in between are approved automatically, but nothing above the root. cade editopens./.cadein$EDITORand allows the current directory afterwardscade statusprovides detailed information on the current cade state
.cade file format
Section titled “.cade file format”one directive per line, # are comments
# discard the ambient (pre-existing) environment for this layer, while still# keeping variables inherited from parent .cade layerspure
# load from flake (default shell or named installable)loadload flakeload flake devShells.default
# load from shell.nixload shellload shell custom-shell.nix
# load from .env fileload envload env .env.development
# load a direnv .envrc (declarative subset only)load envrcload envrc .envrc.local
# run a command and parse its KEY=VALUE output as environmentcall python scripts/get-env.py
# set a variable inline. the key must be ALL_CAPS so it can't be mistaken# for a directive; := for a hard replaceSOMEVAR=somevalue
# inject hooks into cade's lifecycle. these are directly run commandshook preload echo "loading..."hook load echo "ready"hook preunload echo "unloading..."hook unload echo "done"
# unset specific variables (also drops them from inherited layers)clear PYTHONPATH NODE_PATH
# reload this layer when extra files change. useful with `call`watch scripts/get-env.py config/secrets.age
# treat extra variables as colon-lists that accumulate (like PATH)# propagates through layersconcat PYTHONPATH GOPATHin .cade, .env files, and call output, KEY=value follows the variable’s
normal mode, while KEY:=value forces a hard replace that ignores ambient
and parent layers.
direnv compatibility
Section titled “direnv compatibility”cade can read direnv .envrc files, but does not
execute them as shell scripts. it recognizes the declarative subset of
the direnv stdlib that maps cleanly onto cade’s own loaders:
.envrc | cade equivalent |
|---|---|
use flake / use flake .#out | load flake |
use nix [file] | load shell |
dotenv / dotenv_if_exists | load env |
export KEY=value (literal) | sets the variable |
PATH_add dir | prepends dir to PATH |
watch_file f | reloads when f changes |
an .envrc is picked up two ways:
- automatically: a directory with an
.envrcbut no.cadeis treated as if it containedload envrc - explicitly:
load envrc [file]in a.cadecomposes it as one layer alongside other directives
anything cade can’t faithfully reproduce (shell expansion, conditionals,
layout, source_up, functions, unknown flags) is skipped with a warning.
for Nix dev shells, cade captures the final process environment from
nix develop --command rather than parsing nix print-dev-env --json. the JSON
form misses setup done by bash while entering the shell, including shellHook
and devshell PATH changes. the shell-script form of print-dev-env can express
that setup, but would require cade to evaluate bash itself; nix develop keeps
that responsibility inside Nix.
the direnv shim
Section titled “the direnv shim”the reverse is also covered: a direnv shim maps the direnv cli tools rely on
(chiefly direnv export json) onto cade, so direnv-aware tooling such as
editors can drive cade unmodified. this shim is only for tool compatibility;
use cade hook <shell> for interactive shells and cade allow / cade disallow for trust decisions. only export json is mapped. shell hooks and
shell export formats are harmless no-ops so login shell environment capture
keeps working, but they do not activate cade. other direnv commands fail as
unsupported. the shim maps JSON export to cade’s hidden direnv-compatible JSON
endpoint; json is an output format, not a shell accepted by --shell.
the JSON exporter carries minimal DIRENV_DIFF state containing only previous
values for variables cade changed. that state lets repeated exports, directory
leave, and re-enter restore the editor environment without growing PATH or
leaking a full ambient environment snapshot.
enable it in the module with programs.cade.direnvCompat = true;, or build it
from the flake:
nix build .#direnv-compat # produces bin/direnvput it on PATH in place of a real direnv. the packaged shim embeds the cade
store path, so it does not need to find cade through PATH.
example
Section titled “example”given this directory structure:
~/work/.cade # load env .env~/work/project/.cade # load flakewhen you cd ~/work/project, cade loads the .env from ~/work first, then
layers the flake environment from ~/work/project on top.
activation also works from a subdirectory that has no .cade of its own: cade
walks up to the nearest .cade ancestor and activates from there.
variable composition
Section titled “variable composition”cade composes a variable across the ambient environment and each layer, with two behaviors:
- concat (list-like vars).
PATHand other path-like vars (LD_LIBRARY_PATH,*_PATH,MANPATH,XDG_*_DIRS, …) accumulate rather than overwrite. values are ordered child : parent : … : ambient: the innermost layer comes first and wins, and your existing (ambient) value is kept at the end, so system tools stay reachable. mark additional variables list-like withconcat VAR(applies to that layer and inward). - replace (everything else). scalars like
EDITORorCCare replaced; the innermost layer wins and the ambient value is dropped.
two escape hatches:
KEY:=value(in.env/calloutput) forces a hard replace even for a path-like var, with no ambient and no parent layers.purediscards the ambient environment for that layer, so concat vars resolve to the layer stack only (inherited layer values are still kept). shell runtime variables such asPWD,HOME, and command status are preserved so the interactive shell can still run its hooks.
how it works
Section titled “how it works”- the shell hook detects directory changes and calls
cade reload - cade walks up from the current directory to the nearest config directory
(
.cadeor.envrc), then continues up through the contiguous chain of config directories (parent-first), stopping at the first gap - it keeps only the run of directories that are allowed in its SQLite
database (
$XDG_STATE_HOME/cade/cade.db), capping at the first unapproved ancestor, so untrusted layers above your approved base are dropped - it parses and loads each remaining layer’s environment from the configured sources
- layers merge and cade emits shell-specific, safely-quoted commands to stdout
- your shell evaluates the output, setting/unsetting variables and running hooks
- on exit it restores precisely what it changed: variables cade set are
reverted to their prior value or unset, while shell-managed variables
(
PWD,OLDPWD,SHLVL, status, …) and anything you changed mid-session are left untouched. afterpure, the discarded ambient environment is restored from a snapshot.
loaded layers are cached per directory and re-evaluated when a .cade file or
any input it references changes.
license
Section titled “license”EUPL-1.2. see [LICENSE]!(LICENSE)