28 releases (stable)
Uses new Rust 2024
| new 2.0.1 | Apr 22, 2026 |
|---|---|
| 1.12.0 | Apr 21, 2026 |
| 1.7.2 | Mar 7, 2026 |
| 1.7.0 | Feb 17, 2026 |
| 0.1.6 | Feb 11, 2026 |
#23 in Configuration
150KB
3K
SLoC
envoke
Declarative environment variables — any source, any shape, any command. Declare your environments, tags, and overrides in one YAML file. envoke composes values from literals, commands, shell scripts, and templates; resolves template dependencies topologically; and renders the result as shell exports, JSON, a Kubernetes ConfigMap, or anything else a Jinja template can express. Then hand the resolved variables to a command:
envoke exec prod -- psql # exec with resolved vars overlaid
envoke render prod --output .env # write a .env file
envoke render prod --format json # render as JSON
envoke render prod --format k8s-secret # render as a Kubernetes Secret manifest
envoke render prod --template custom.j2 # render any shape you want
Why envoke?
envoke is a composer and renderer for environment variables, not a secret store. The niche it fills is the intersection of multi-source composition (literals, commands, shell, templates), per-environment / per-tag / per-override variation, and deterministic output to any shape. Pair it with your secret store of choice — fnox, SOPS, 1Password CLI, or Vault — and let envoke handle the composition:
# envoke.yaml
DB_PASS:
envs:
prod:
sh: op read "op://prod/db/password" # fetched via 1Password CLI
local:
literal: devpassword
envoke doesn't hook into the shell like direnv or other .env loaders, but produces structured output and renders flexibly.
It also doesn't encrypt or cache, but is composable with tools that do, via the sh and cmd sources.
Installation
With mise (recommended)
mise use -g github:glennib/envoke
From crates.io
cargo install envoke-cli
With cargo-binstall
cargo binstall envoke-cli
With mise (from crates.io)
mise use -g cargo:envoke-cli
From source
cargo install --git https://github.com/glennib/envoke envoke-cli
From GitHub releases
Pre-built binaries are available on the releases page for:
- Linux (x86_64, aarch64)
- macOS (x86_64, Apple Silicon)
- Windows (x86_64)
Quick start
Create an envoke.yaml:
variables:
DB_HOST:
default:
literal: localhost
envs:
prod:
literal: db.example.com
DB_USER:
default:
literal: app
DB_PASS:
envs:
local:
literal: devpassword
prod:
sh: vault kv get -field=password secret/db
DB_URL:
default:
template: "postgresql://{{ DB_USER }}:{{ DB_PASS | urlencode }}@{{ DB_HOST }}/mydb"
Generate variables for an environment:
$ envoke render local
# @generated by `envoke render local` at 2025-06-15T10:30:00+02:00
# Do not edit manually. Modify envoke.yaml instead.
DB_HOST='localhost'
DB_PASS='devpassword'
DB_URL='postgresql://app:devpassword@localhost/mydb'
DB_USER='app'
Note: Output is sorted alphabetically by variable name. All output includes an
@generatedheader with the invocation command and timestamp. Examples below omit this header for brevity.
Or hand them straight to a command — no shell dance required:
envoke exec local -- psql
envoke exec prod -- kubectl apply -f manifest.yaml
Alternatively, source them into your shell:
eval "$(envoke render local --format shell-export)"
Or write them to a file:
envoke render local --output .env
Tip:
renderhas aliasrandexechas aliasx, soenvoke r local,envoke x prod -- psql, etc. also work.
Running commands with resolved variables
The exec subcommand runs a subprocess with the resolved variables
overlaid on envoke's own environment. Everything after -- is passed
verbatim to the child:
envoke exec prod -- psql
envoke exec prod -- sh -c 'echo "$DATABASE_URL"'
envoke exec local -- npm run dev
Overlay semantics. The child inherits envoke's process environment
(PATH, HOME, TERM, SSH_AUTH_SOCK, …) and the resolved variables are
layered on top — any inherited variable with the same name as a resolved one is
replaced. Variables not declared in envoke.yaml pass through unchanged.
Process model. On Unix, envoke replaces itself with the target process via
execvp — the child keeps envoke's PID, TTY, and signal disposition, so
Ctrl-C and SIGTERM behave exactly as if you had invoked the command
directly. On other platforms, envoke spawns the child and forwards its exit
code.
Subcommand separation. Output-shaping flags (--output, --template,
--format) live on render; the trailing -- <command> lives on
exec. Pick the subcommand that matches your intent.
Shell integration with mise
If your project uses mise to manage tools, the
envoke-env plugin activates envoke
on shell entry — no manual eval or sourcing needed.
# mise.toml
[tools]
"github:glennib/envoke" = "2.0.0"
[plugins]
envoke = "https://github.com/glennib/envoke-env#v2.0.0"
[env]
_.envoke = { fallback_environment = "local", tools = true }
Put the target environment name in .envoke-env (gitignored):
staging
When mise activates, the plugin runs envoke render staging against your
envoke.yaml and injects the resolved variables. Switch environments with
echo prod > .envoke-env; the plugin watches the file and re-evaluates
on the next activation. Tags and overrides can be added on subsequent lines
(tag:vault, override:read-replica).
See the envoke-env README for caching, the fallback environment, and other configuration options.
Configuration
The config file (default: envoke.yaml) has a single top-level key variables
that maps variable names to their definitions.
Variable definition
Each variable can have:
| Field | Description |
|---|---|
description |
Optional. Rendered as a # comment above the variable in output. |
tags |
Optional. List of tags for conditional inclusion. Variable is only included when at least one of its tags is passed via --tag. Untagged variables are always included. |
default |
Optional. Fallback source used when the target environment has no entry in envs. |
envs |
Map of environment names to sources. |
overrides |
Optional. Map of override names to alternative source definitions (each with its own default/envs). Activated via --override. |
A variable must have either an envs entry matching the target environment or a
default. If neither exists, resolution fails with an error.
Source types
Each source specifies exactly one of the following fields:
literal
A fixed string value.
DB_HOST:
default:
literal: localhost
cmd
Run a command and capture its stdout (trimmed). The value is a list where the first element is the executable and the rest are arguments.
GIT_SHA:
default:
cmd: [git, rev-parse, --short, HEAD]
sh
Run a shell script via sh -c and capture its stdout (trimmed).
TIMESTAMP:
default:
sh: date -u +%Y-%m-%dT%H:%M:%SZ
template
A minijinja template string, compatible
with Jinja2. Reference other variables
with {{ VAR_NAME }}. Dependencies are automatically detected and resolved first
via topological sorting.
DB_URL:
default:
template: "postgresql://{{ DB_USER }}:{{ DB_PASS }}@{{ DB_HOST }}/{{ DB_NAME }}"
A meta object is available in variable templates with the following fields:
| Field | Description |
|---|---|
meta.environment |
The target environment name passed to envoke. |
API_URL:
default:
template: "https://{{ meta.environment }}.example.com/api"
$ envoke render staging
API_URL='https://staging.example.com/api'
All minijinja built-in filters
are available (upper, lower, replace, trim, default, join, etc.), plus
the following additional filters:
urlencode-- percent-encodes special characters for use in URLs.shell_escape-- escapes single quotes for shell safety ('->'\'').dotenv_escape-- encodes a value as a portable.envtoken with delimiters included (single-quoted when safe, else double-quoted with conservative escapes).
CONN_STRING:
default:
template: "postgresql://{{ USER | urlencode }}:{{ PASS | urlencode }}@localhost/db"
APP_NAME_LOWER:
default:
template: "{{ APP_NAME | lower }}"
skip
Omit this variable from the output. Useful for conditionally excluding a variable in certain environments while including it in others.
DEBUG_TOKEN:
default: skip
envs:
local:
literal: debug-token-value
Environments and defaults
envoke selects the source for each variable by checking the envs map for the
target environment. If no match is found, it falls back to default. This lets
you define shared defaults and override them per environment:
LOG_LEVEL:
default:
literal: info
envs:
local:
literal: debug
prod:
literal: warn
Tags
Tags gate variables behind explicit opt-in. The typical use case: your config
includes a VAULT_SECRET whose value is fetched by an expensive sh: vault kv get … command. You don't want that command to run every time someone runs
envoke local during day-to-day development — only when they actually need the
secret. Tag it with vault, and the variable is included only when --tag vault is passed.
Untagged variables are always included. Tagged variables are only included
when at least one of their tags is passed via --tag. This keeps expensive
resolvers (vault lookups, cloud API calls, slow shell scripts) out of the hot
path.
variables:
DB_HOST:
default:
literal: localhost
VAULT_SECRET:
tags: [vault]
envs:
prod:
sh: vault kv get -field=secret secret/app
local:
literal: dev-secret
OAUTH_CLIENT_ID:
tags: [oauth]
envs:
prod:
sh: vault kv get -field=client_id secret/oauth
local:
literal: local-client-id
# Without --tag, only untagged variables are included:
$ envoke render local
DB_HOST='localhost'
# Include vault-tagged variables (and all untagged ones):
$ envoke render local --tag vault
DB_HOST='localhost'
VAULT_SECRET='dev-secret'
# Include everything:
$ envoke render local --tag vault --tag oauth
DB_HOST='localhost'
OAUTH_CLIENT_ID='local-client-id'
VAULT_SECRET='dev-secret'
Variables without tags are always included regardless of which --tag flags are
passed. Tagged variables require explicit opt-in.
Overrides
Overrides let you point a single variable at a different source without
duplicating an entire environment. Classic example: you have a prod
environment, and occasionally you want DATABASE_HOST to point at a
read-replica instead of the primary — but everything else (port, credentials,
cache strategy) should stay identical. Creating a whole prod-read-replica
environment duplicates ten other variables for the sake of one. Instead,
declare a read-replica override on DATABASE_HOST and activate it with
--override read-replica:
envoke exec prod -- psql # primary
envoke exec prod --override read-replica -- psql # same env, replica host
Overrides are the third dimension alongside environments and tags. A variable
can declare named overrides, each with its own default/envs sources.
Activate them with --override:
variables:
DATABASE_HOST:
default:
literal: localhost
envs:
prod:
literal: 172.10.0.1
overrides:
read-replica:
default:
literal: localhost-ro
envs:
prod:
literal: 172.10.0.2
CACHE_STRATEGY:
envs:
prod:
literal: lru
overrides:
aggressive-cache:
envs:
prod:
literal: lfu-with-prefetch
DATABASE_PORT:
default:
literal: "5432"
# No overrides -- unaffected by --override flag
# Base values:
$ envoke render prod
CACHE_STRATEGY='lru'
DATABASE_HOST='172.10.0.1'
DATABASE_PORT='5432'
# Activate an override:
$ envoke render prod --override read-replica
CACHE_STRATEGY='lru'
DATABASE_HOST='172.10.0.2'
DATABASE_PORT='5432'
# Multiple overrides on disjoint variables:
$ envoke render prod --override read-replica --override aggressive-cache
CACHE_STRATEGY='lfu-with-prefetch'
DATABASE_HOST='172.10.0.2'
DATABASE_PORT='5432'
When an override is active for a variable, the source is selected using a 4-level fallback chain:
- Override
envs[environment] - Override
default - Base
envs[environment] - Base
default
Variables without a matching override definition are unaffected and use the normal base fallback. If multiple active overrides are defined on the same variable, envoke reports an error. Unknown override names (not defined on any variable) produce a warning on stderr.
CLI usage
envoke [GLOBAL OPTIONS] <SUBCOMMAND>
Subcommands
| Subcommand | Alias | Purpose |
|---|---|---|
render <ENV> |
r |
Resolve variables and print them (or write to a file). |
exec <ENV> -- <COMMAND>... |
x |
Resolve variables and exec a command with them overlaid. |
meta <WHAT> |
— | Enumerate names of a config dimension: environments, tags, overrides, or all (prefixed). |
schema |
— | Print the JSON Schema for envoke.yaml. |
completions <SHELL> |
— | Print shell completions (bash, zsh, fish, elvish, powershell). |
Global options
Usable before or after the subcommand.
| Option | Description |
|---|---|
-c, --config <PATH> |
Path to config file. Default: envoke.yaml. |
-t, --tag <TAG> |
Only include tagged variables with a matching tag. Repeatable. Untagged variables are always included. |
--all-tags |
Include every tagged variable regardless of its tags. Conflicts with --tag. |
-O, --override <NAME> |
Activate a named override for source selection. Repeatable. Per variable, at most one active override may be defined. |
--no-parallel |
Resolve cmd: and sh: sources serially instead of in parallel. |
-q, --quiet |
Suppress informational messages on stderr. |
Global repeatables and the subcommand boundary.
--tagand--overrideare repeatable globals. Specifying them on both sides of the subcommand is a footgun — the occurrences after the subcommand replace (not append to) any occurrences before it. For example,envoke --tag a render prod --tag bresults intags = ["b"], not["a", "b"]. Pick one side.
render options
| Option | Description |
|---|---|
<ENV> |
Target environment name (e.g. local, prod). Can also be set via the ENVOKE_ENV environment variable. |
-o, --output <PATH> |
Write output to a file instead of stdout. |
-f, --format <FORMAT> |
Select a built-in output preset: dotenv (default), shell-export, json, yaml, k8s-secret, github-actions, terraform-tfvars. See Output formats. Conflicts with --template. |
--template <PATH> |
Use a custom output template file instead of a preset. See Custom templates. |
exec options
| Option | Description |
|---|---|
<ENV> |
Target environment name. Can also be set via the ENVOKE_ENV environment variable. |
-- <COMMAND>... |
Command to exec with resolved variables overlaid. See Running commands. The -- separator is required. |
Environment variables
| Variable | Description |
|---|---|
ENVOKE_ENV |
Fallback for the <ENV> positional on render and exec. |
JSON Schema
Generate a JSON Schema for editor autocompletion and validation:
envoke schema > envoke-schema.json
Use it in your envoke.yaml with a schema comment for editors that support it:
# yaml-language-server: $schema=envoke-schema.json
variables:
# ...
Alternatively, point directly at the hosted schema without writing a local file:
# yaml-language-server: $schema=https://raw.githubusercontent.com/glennib/envoke/refs/heads/main/envoke.schema.json
variables:
# ...
How it works
- Parse the YAML config file.
- Filter out variables excluded by
--tagflags (if any). - For each remaining variable, select the source matching the target environment
(or the default), applying the override fallback chain if
--overrideflags are active. - Extract template dependencies and topologically sort all variables using Kahn's algorithm.
- Resolve values in dependency order -- literals are used as-is, commands and shell scripts are executed, templates are rendered with already-resolved values.
- Render output using a built-in or custom Jinja2 template (see
Custom templates). The default template produces an
@generatedheader followed by sortedVAR='value'lines in the.envdotenv format.
Circular dependencies and references to undefined variables are detected before any resolution begins and reported as errors.
Output formats
--format <FORMAT> selects a curated built-in preset. The defaults cover the
shapes most projects need; for anything else, use --template.
| Format | Output shape | Typical use |
|---|---|---|
dotenv (default) |
KEY='value' when safe, else KEY="value" with conservative escapes (\\, \", \$, \n). $ never expands at the consumer. |
.env files consumed by dotenvy (mise, Rust), godotenv (Docker Compose), python-dotenv, node dotenv |
shell-export |
export KEY='value' |
Source into a POSIX shell for children to inherit: source <(envoke render local --format shell-export) |
json |
Compact JSON object | Feeding structured tools; pipe through jq . for pretty output |
yaml |
YAML mapping (block style) | Human-readable config files, yq pipelines |
k8s-secret |
Kubernetes Secret manifest with stringData: |
envoke render prod --format k8s-secret | kubectl apply -f - |
github-actions |
Heredoc blocks for $GITHUB_ENV |
- run: envoke render prod --format github-actions >> "$GITHUB_ENV" |
terraform-tfvars |
HCL KEY = "value" |
envoke render prod --format terraform-tfvars > prod.auto.tfvars |
Notes.
--format jsonoutput is also valid YAML 1.2, so use it when you want compact structured output.k8s-secretderivesmetadata.namefrom the environment (lowercased,_replaced with-). For exotic env names, post-process or use--template.- Some
.envparsers (e.g.dotenvx) expand$VARinside double-quoted values. A value likepa$wordmay not round-trip through those.
Custom templates
If none of the presets fit, supply your own
minijinja (Jinja2-compatible)
template via --template:
envoke render local --template my-template.j2
Template context
The template receives the following variables:
| Name | Type | Description |
|---|---|---|
variables |
map of name -> {value, description} |
Rich access: {{ variables.DB_URL.value }}. Iteration: {% for name, var in variables | items %}. Sorted alphabetically. |
v |
map of name -> value string | Flat shorthand: {{ v.DATABASE_URL }}. |
meta.timestamp |
string | RFC 3339 timestamp of invocation. |
meta.invocation |
string | Full CLI invocation as a single string. |
meta.invocation_args |
list of strings | CLI args as individual elements. |
meta.environment |
string | Target environment name. |
meta.config_file |
string | Path to the config file used. |
Filters
All minijinja built-in filters
are available (upper, lower, replace, trim, default, join, length,
first, last, sort, unique, tojson, etc.), plus these additional filters:
shell_escape-- escapes single quotes for shell safety ('->'\'').dotenv_escape-- encodes a value as a portable.envtoken with delimiters included (single-quoted when safe, else double-quoted with conservative escapes\\,\",\$,\n;$is never expanded at the consumer).urlencode-- percent-encodes special characters.
All filters are available in both variable templates (the template source type)
and custom output templates.
Example: JSON output
{
{% for name, var in variables | items %} "{{ name }}": "{{ var.value }}"{% if not loop.last %},{% endif %}
{% endfor %}}
envoke render local --template json.j2
Note: This simplified example does not escape JSON special characters (
",\, newlines) in values. For production use, consider a template that handles escaping.
Example: Docker .env format
# Generated for {{ meta.environment }}
{% for name, var in variables | items -%}
{{ name }}={{ v[name] }}
{% endfor -%}
Note: This simplified example does not quote or escape values. Values containing
=,#, or whitespace may not parse correctly in all.envimplementations.
Example: Kubernetes ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ meta.environment | lower }}-env
labels:
app: myapp
environment: {{ meta.environment | lower }}
generated-by: envoke
data:
{% for name, var in variables | items %} {{ name }}: "{{ var.value }}"
{% endfor %}
This example uses lower and items filters to generate a Kubernetes-compatible
manifest directly from your envoke config.
Shell completions
Generate completions for your shell:
# Bash
envoke completions bash > ~/.local/share/bash-completion/completions/envoke
# Zsh
envoke completions zsh > ~/.zfunc/_envoke
# Fish
envoke completions fish > ~/.config/fish/completions/envoke.fish
Development
This project uses mise as a task runner. After installing mise:
mise install # Install tool dependencies
mise run build # Build release binary
mise run test # Run tests (via cargo-nextest)
mise run clippy # Run lints
mise run fmt # Format code
mise run ci # Run all checks (fmt, clippy, test, build)
Run a single test:
cargo nextest run -E 'test(test_name)'
Debugging
envoke uses tracing for diagnostic output. Set the
RUST_LOG environment variable to see debug messages on stderr:
RUST_LOG=debug envoke render local
This is useful for troubleshooting tag filtering, override fallback chains, and source resolution order.
License
MIT OR Apache-2.0
Dependencies
~24MB
~370K SLoC