| name | envlit |
| description | Use when you need to load project-specific environment variables or the user needs help with envlit CLI usage, `.envlit/*.yaml` profiles, dynamic flags, PATH operations, lifecycle hooks, or environment restore/state-tracking issues involving `envlit`, `el`, or `eul`. |
envlit — Environment Profile Manager
envlit lets you define named environment profiles in YAML files, then load/unload them with a single shell command. It tracks what it changed and intelligently restores your original environment, preserving any manual changes you made in between.
When to use envlit
- Switching between dev/staging/prod configs in the same shell
- ML/AI workflows: manage
CUDA_VISIBLE_DEVICES, model paths, backends per experiment
- Multi-project setups: each project has its own
.envlit/ folder committed to git
- Team consistency: share base configs via git, keep personal overrides locally
Installation & Shell Setup
pip install envlit
eval "$(envlit init)"
envlit init outputs shell function definitions for el (load) and eul (unload). Without this line, el/eul are not available.
Customize the alias names:
eval "$(envlit init --alias-load myload --alias-unload myunload)"
Core Commands
| Command | What it does |
|---|
el | Load .envlit/default.yaml |
el <profile> | Load .envlit/<profile>.yaml |
el <profile> --flag value | Load with dynamic flag overrides |
el --config /path/to/any.yaml | Load an arbitrary config file (bypasses .envlit/ lookup) |
eul | Unload and restore original environment |
envlit state | Print tracked variables in KEY=VALUE format |
envlit state --from-env | Print current shell values for tracked vars |
envlit state > .env | Export tracked vars to a .env file |
envlit doctor | Diagnose installation and config issues |
--config / -c flag
el and eul accept --config (short: -c) to load any YAML file regardless of location:
el --config ~/shared/base.yaml
el dev --config /tmp/override.yaml
envlit load --config path/to/config.yaml
envlit state output format
Output is one KEY=VALUE per line, sorted alphabetically. Values containing spaces, tabs, $, backticks, quotes, or backslashes are wrapped in double quotes with special characters escaped. Plain values (only letters, digits, /, :, ., -, _, etc.) are output bare:
API_URL=https://api.example.com
DEBUG=true
PATH=/home/user/.local/bin:/usr/bin:/bin
PROJECT_DIR="/home/user/my project"
In the example above, PATH has no special characters so it is unquoted; PROJECT_DIR contains a space so it is double-quoted.
Config File Format (.envlit/<profile>.yaml)
Config files live in a .envlit/ directory in your project root.
Full annotated example
extends: "./base.yaml"
dotenv:
- "./.env"
- "./.env.local"
env:
DEBUG: "true"
API_URL: "https://api.example.com"
CUDA_VISIBLE_DEVICES:
flag: ["--cuda", "-g"]
default: "0"
ML_COMPUTE_BACKEND:
flag: ["--backend", "-b"]
default: "c"
map:
c: "CPU"
g: "GPU"
t: "TPU"
PYTHONPATH:
op: prepend
value: "./src"
OLD_VAR:
op: unset
PATH:
- op: remove
value: "/deprecated/bin"
- op: prepend
value: "./bin"
- op: append
value: "/opt/tools"
DATA_DIR: "${HOME}/data"
PRICE_TAG:
op: set
value: "$9.99"
interpolate: false
REMOVED_VAR: null
hooks:
pre_load:
- name: "Check docker"
script: "command -v docker >/dev/null || echo 'Warning: docker not found'"
- name: "Multi-line setup"
script: |
echo 'Starting setup...'
mkdir -p /tmp/myapp
echo 'Done'
post_load:
- name: "Show status"
script: "echo 'Loaded. DEBUG=${DEBUG}, MODE=${PROJECT_MODE}'"
pre_unload:
- name: "Cleanup"
script: "echo 'Unloading...'"
post_unload:
- name: "Confirm"
script: "echo 'Environment restored'"
Dotenv precedence: inherited values are loaded first, followed by dotenv files in list order, then the current profile's env: values. Cross-source replacements warn on stderr without exposing values. Envlit does not auto-discover dotenv files, and a declared file that is missing or malformed stops the load.
Hook execution order during load:
pre_load hooks run first (env vars NOT yet set — see pre-load state)
- Environment variables are exported
post_load hooks run (env vars ARE set — safe to reference $MY_VAR)
Hook execution order during unload:
pre_unload hooks run (env vars still set from the load)
- Environment is restored to original state
post_unload hooks run (env vars are unset/restored)
Environment Variable Operations
| Operation | When to use | Required fields |
|---|
set | Set a value (also the string shorthand) | value |
unset | Remove a variable entirely | — |
prepend | Insert at the front of a : separated list | value |
append | Insert at the back of a : separated list | value |
remove | Remove an entry from a : separated list | value |
All operations support an optional separator field (default :):
env:
PYTHONPATH:
op: prepend
value: "./src"
separator: ":"
interpolate field (on single dict ops only, default true):
true — $VAR and ${VAR} in the value expand at shell runtime
false — value is treated as a literal string; $ is never expanded
Important: interpolate is only recognised on a single dict operation ({op: set, value: ..., interpolate: false}). It is not supported inside a list pipeline — list steps always use double-quote (interpolate: true) mode. If you need a literal $ in a pipeline, restructure to a single dict op.
env:
BIN_DIR:
op: set
value: "${HOME}/.local/bin"
interpolate: true
PRICE:
op: set
value: "$9.99"
interpolate: false
null shorthand: A YAML null value is equivalent to op: unset:
env:
OLD_API_KEY: null
Env var name constraint: names must match [a-zA-Z_][a-zA-Z0-9_]*. Names with hyphens (e.g. MY-VAR) are rejected.
Dynamic Flags
Flags are defined inline inside env: entries. The variable name is the key; add a flag: field to make it a CLI option:
env:
CUDA_VISIBLE_DEVICES:
flag: ["--cuda", "-g"]
default: "0"
ML_COMPUTE_BACKEND:
flag: ["--backend", "-b"]
default: "c"
map:
c: "CPU"
g: "GPU"
t: "TPU"
el dev
el dev --cuda 2
el dev -g 0,1 -b g
el dev --help
Inline flag fields (all inside the env: entry):
flag — CLI option name(s): a single string ("--cuda") or list (["--cuda", "-g"])
default — value used when the flag is not passed on the CLI. A value passed on the CLI always overrides the default.
map (optional) — dict mapping CLI input → env var value. Unknown values are passed through as-is (no error).
Deprecated: The top-level flags: section (with target: field) still works but emits a deprecation warning. Migrate flags into env: entries as shown above.
Config Inheritance
extends: "./base.yaml"
env:
DEBUG: "true"
Merge rules:
env: child wins (shallow merge per key)
flags: child wins (shallow merge per key)
hooks: lists are concatenated (base hooks run first)
Multi-level chaining is supported: dev.yaml → staging.yaml → base.yaml all resolve correctly. Each file's extends is resolved before merging, so the full ancestor chain is flattened in order.
State Tracking
envlit uses a Compare-and-Swap algorithm so it never clobbers manual changes:
el dev
export DEBUG=verbose
eul
Three scenarios on unload:
- Variable was unset before load → unset it again
- Variable was set before load, not touched since → restore original value
- You modified it after load → leave it at your value (not restored)
eul works only in the same shell session as el. Opening a new terminal loses the tracking state.
Running el twice without eul: Safe. The CAS algorithm detects that each variable's value matches what the previous el set (no manual interference), keeps the original pre-first-load value, and just updates current. eul restores to the state before the first el in the session.
eul with no prior el: If eul is run in a shell where el was never called (no tracking state exists), it is a no-op — it outputs a comment and makes no changes.
No parent-directory traversal: el only looks for .envlit/ in the current working directory. It does not walk up to parent directories. Run el from your project root (where .envlit/ lives), or use --config to point at a file by path.
Config Inheritance Pattern (Multi-profile Setup)
.envlit/
├── base.yaml # shared: API URLs, common tools
├── dev.yaml # extends base, debug flags
└── prod.yaml # extends base, prod settings
env:
APP_NAME: "myapp"
LOG_FORMAT: "json"
hooks:
post_load:
- name: "Banner"
script: "echo 'Loaded ${APP_NAME}'"
extends: "./base.yaml"
env:
DEBUG: "true"
LOG_LEVEL: "debug"
API_URL: "http://localhost:8000"
extends: "./base.yaml"
env:
DEBUG: "false"
LOG_LEVEL: "warn"
API_URL: "https://api.example.com"
Special Characters Reference
| YAML value | Shell sees | Notes |
|---|
"$HOME/data" | /Users/you/data | Shell expands $HOME (interpolate: true) |
"${PATH:-/usr/bin}" | default expansion | Full ${VAR:-default} syntax supported |
"price $9.99" | price $9.99 (⚠ $9 expands!) | Use interpolate: false for literal $ |
"Use \ls`"` | Use `ls` | Backticks auto-escaped in double-quote mode |
'has "double" quotes' | has "double" quotes | YAML single-quote wrapping |
${VAR:-${OTHER}} (nested expansions) are not supported — the inner } terminates the match early.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|
el: command not found | Shell not initialized | Add eval "$(envlit init)" to .bashrc/.zshrc, then source ~/.zshrc |
| Config not loading | Wrong filename | File must be .envlit/<profile>.yaml (or .yml), in the current directory |
Dynamic flags missing from --help | YAML syntax error in env: flag entry | Check indentation; flag: must be inside an env: entry; run envlit doctor |
Variables not restored after eul | Different shell session | eul only works in the shell that ran el |
Invalid environment variable name | Hyphen/space in YAML key | Rename key to match [a-zA-Z_][a-zA-Z0-9_]* |
| Hook not running | Wrong key name | Hook keys are pre_load, post_load, pre_unload, post_unload (underscore) |
Run envlit doctor for automated diagnosis.
How to Help Users
- New setup → guide through
pip install envlit, eval "$(envlit init)", create .envlit/default.yaml
- YAML errors → check hook key names (underscores), flag field names (
flag/target/map), valid env var names
- State confusion → explain CAS algorithm;
eul only works in same shell session
- Flags not working → use inline flag syntax in
env: (not deprecated top-level flags:); verify flag is a list, map keys match what user passes on CLI
- PATH operations → use pipeline syntax (list of dicts);
separator defaults to :
Links