Temporary access to secrets and private networks for one command, then gone. Same wrap for humans and harnesses. See which access was used.
A command needs secrets or a private network. That access should
exist for the process, then disappear. One lade.yaml. Three
families: secret, tunnel, package. Install, lade setup, type
the command.
Lade on Fish, Bash, or Zsh. macOS and Linux. Cursor, Claude Code, Codex, and OpenCode.
curl -fsSL https://raw.githubusercontent.com/zifeo/lade/main/installer.sh | bash
cd your-repo
lade setupSetup installs a shell hook (pre-exec) once, in your profile, so every command you type can match. The rest stays in the repo: pre-tool hooks for Cursor, Claude Code, Codex, and OpenCode harnesses, and the lock for pinned packages. The wrap is yours. The access is this repo's.
Humans use the shell hook (pre-exec). Harnesses use pre-tool hooks in
the repo. Same lade.yaml, same resolve.
You do not prefix the command. Without a wrap: lade -- tofu apply.
lade on / lade off pause pre-exec in this shell.
lade hook enable --shell installs pre-exec in this shell's profile.
lade teardown removes this repo's pre-tool hooks and runs
?teardown= commands. The shell hook stays.
Or from GitHub:
cargo install --git https://github.com/zifeo/lade --locked|
pre-exec. Run commands normally. Lade injects access only when
the command matches |
|
|
Provider resolution. Match commands and load values from vaults, files, or inline config only when needed. |
|
|
In-memory cache. A second match reads the hub. Progress tags
|
|
|
Private networks. Open a local forward only while the command runs, then close it automatically. |
|
|
Secrets as files. Write temporary config files for commands that expect credentials on disk. |
|
|
One command. Use |
|
|
Human approval. A |
|
|
Intermediate bindings. |
More: examples/tape/.
A lade.yaml (or lade.yml) at the folder that owns the command.
Lade walks from the current directory up to $HOME. The nearest
file wins on a key. The parent is the default.
The file may pin the required Lade range with an empty YAML key
(: >=0.18.0). An empty command regex is not a rule. Use . to
match every command. Edit : by hand when the repo needs a Lade
that is new enough. There is no pin command. lade upgrade still
runs if this binary is too old.
: >=0.18.0
"^tofu":
TF_VAR_api_key: op://DOMAIN/VAULT/ITEM/FIELDtofu apply| Family | What |
|---|---|
secret |
A value for the command (op://, file://, raw, …). Lands in the process env |
tunnel |
A local forward for the process (kubectl://, tsh://, …) |
package |
A pinned CLI or setup package (mise://, apm://, skills://) |
lade add secret / lade add tunnel / lade add package write the
nearest yaml, then run lade setup. lade remove drops a binding and
does not run setup.
Family aliases (CLI only): env → secret; net / network / fwd
→ tunnel; pkg / cli / tool / apm / skill → package.
A vault, a file, a shell snippet, or a raw string. Provider-resolved
values are masked unless --no-mask. Raw is not a vault secret.
It is a value you put in the yaml. lade add warns once. A wrap
does not. It still lives in this family because it becomes an env
var.
"^psql":
DB_USER: op://my.1password.com/eng/postgres/username
DATABASE_URL: postgres://${DB_USER}@127.0.0.1:${DB_PORT}/app| Provider | URI | Notes |
|---|---|---|
| 1Password | op://DOMAIN/VAULT/ITEM/FIELD |
Optional section. 1Password CLI. |
| Infisical | infisical://DOMAIN/PROJECT_ID/ENV_NAME/SECRET_NAME |
Nested folders. Infisical CLI. |
| Doppler | doppler://DOMAIN/PROJECT_NAME/ENV_NAME/SECRET_NAME |
Doppler CLI. |
| Vault | vault://DOMAIN/MOUNT/KEY/FIELD |
KV v2. VAULT_TOKEN or ~/.vault-token. Setup locks the Vault CLI. |
| Passbolt | passbolt://DOMAIN/RESOURCE_ID/FIELD |
Passbolt CLI. |
| Bitwarden | bw://ITEM/FIELD |
BW_SESSION after bw unlock. |
| AWS Secrets Manager | awssm://REGION/NAME |
Optional ?query=, ?version=. |
| Azure Key Vault | azurekv://VAULT/NAME |
Optional ?query=. |
| GCP Secret Manager | gcpsm://PROJECT/NAME |
Optional ?query=, ?location=. |
| age | age://CIPHERTEXT |
?plugin=, ?identity=. |
| SOPS | sops://PATH?query=.field |
?plugin= remaps env into the SOPS child. |
| File | file://PATH?query=.fields[0].field |
?query= required. INI, JSON, YAML, TOML. |
| Shell | sh://gcloud auth print-access-token |
Also bash://, zsh://, fish://. |
| Raw | "visible-in-the-yaml" |
Not a secret. ! forces raw, !! keeps a leading !. |
Vault, file, sops, and age stay in RAM for 5 minutes after the
first hydrate. Set ttl: on that rule's . to turn it off or
raise it, up to 24 hours. Shell URIs stay off unless that body
sets ttl:. Raw, tunnels, and packages never cache.
lade cache set 2h marks this cwd on the hub. Parent folders
share it. ttl: off on the body still wins. lade cache unset
clears this cwd.
lade cache forget drops the RAM now.
"^psql":
.:
ttl: 1h
DB_USER: op://my.1password.com/eng/postgres/username
"^curl":
.:
ttl: off
API_TOKEN: op://DOMAIN/VAULT/ITEM/FIELDlade eval <uri> resolves one URI. Authenticate the provider CLI
first. Lade does not pick the login command. See that product's
docs, then retry.
.file on the rule writes a temp JSON/YAML for the command, then
deletes it. That is output, not the file provider.
"deploy .*":
.:
file: secrets.yml
API_TOKEN: op://DOMAIN/VAULT/ITEM/FIELDA local forward for this process. An env-var key gets an ephemeral
local port unless local= sets one. A numeric key is a fixed port.
"^psql":
DB_PORT: kubectl://k8s.example.com:6443/prod/default/service/postgres/5432
1223: ssh://jump.example.com:22/db.internal/5432| Provider | URI | Query |
|---|---|---|
kubectl |
kubectl://<host>:<port>/<context>/<ns>/<kind>/<name>/<remote-port> |
local=HOST:PORT, pod-running-timeout= |
kubefwd |
kubefwd://<host>:<port>/<context>/<ns>/<kind>/<name>/<service-port> |
local=, domain=, selector= |
tsh |
tsh://<proxy>:<port>/app/<app-name>[/<target-port>] or .../kube_cluster/<cluster>/<ns>/<kind>/<name>/<remote-port> |
local= |
ssh |
ssh://<jump>:<port>/<remote-host>/<remote-port> |
local= |
A CLI for the matched command, or a setup-only package. The key
is the argv0 you type. If the binary is tofu1.8, the key is
tofu1.8. Lade asks the tool manager to pin it, then puts that
install first on PATH for this process.
^tofu:
tofu1.8: mise://aqua/opentofu/opentofu@1.8.2
TF_VAR_FOO: op://DOMAIN/VAULT/ITEM/FIELDToday the URI scheme is mise://. That is how the current
manager is addressed. A Homebrew binary on PATH is not the pin.
lade setup walks toward $HOME once. A committed mise.toml
or mise.lock in the repo is the plane: setup extends both.
$HOME itself counts only for ~/lade.yaml. Otherwise one
lade.lock at the git root. A git repo in $HOME is not that
root. Missing slots are resolved once and written.
| Command | Lock | Yaml |
|---|---|---|
lade setup |
Install this version | Exact pins stay. A range may heal from mise.toml |
lade setup --unlock |
Ignore it this once, then rewrite | Resolve and install what yaml says now |
lade update |
Rewrite after a new resolve | Exact pins stay. Implied and ranged pins move to the latest match |
lade upgrade |
Untouched | Untouched. This is the Lade binary |
lade teardown is this repo only: pre-tool hooks and ?teardown=
on setup packages. It does not remove the shell wrap.
Optional ?setup= / ?teardown= run on lade setup /
lade teardown in this repo.
.:
dcg: mise://github:Dicklesworthstone/destructive_command_guard@0.6.6?setup=install&teardown=uninstall| Scheme | URI | What |
|---|---|---|
mise |
mise://<backend>/<package>@<version> |
A CLI pin. lade add package writes this. |
apm |
apm://<registry-path> |
Setup-rule package only. Not a command pin. |
skills |
skills://<registry-path> |
Setup-rule package only. Not a command pin. |
Walk cwd → $HOME. Later file overlays the same key. Child wins.
Parent is the default. A command in app/ uses app/lade.yaml
over the repo root. ~/lade.yaml is a parent like any other.
YAML ~ on a key cancels a parent value. ${NAME} composes
bindings in the same rule. .NAME is intermediate: used to
build another value, not exported.
Same directory must not have both lade.yaml and lade.yml.
Opt-in. log: true on a rule records that the command ran, what
matched, which package resolved. Values are never stored. The row is
an audit snapshot at write time.
.:
.:
log: truelade log
lade usage
lade statusQueries stay on this git root. --all / --global reads wider.
Details: docs/observability.md.
Same yaml. Same resolve. Humans type commands in a wrapped shell
(pre-exec). Harnesses run commands through pre-tool hooks.
lade setup writes both when needed. Limit harness installs with
lade setup --harness cursor (repeat for several). If pre-exec is
missing later, lade status and setup point at
lade hook enable --shell. Harness hooks:
lade hook enable --harness <slug> (recovery; hidden from default
--help, see docs/protocol.md).
when is always (default), human, or agent.
"^git ":
- .:
when: human
SSH_AUTH_SOCK: sh://launchctl getenv SSH_AUTH_SOCK
- .:
when: agent
SSH_AUTH_SOCK: 'sh://printf %s "$HOME/.ssh/agent.sock"'"deploy .*":
API_TOKEN:
alice: op://DOMAIN/VAULT/ALICE_TOKEN/FIELD
.: op://DOMAIN/VAULT/DEFAULT_TOKEN/FIELDlade user aliceA disclaimer withholds access. Review, then lade approve <code>.
User-facing verbs. Internal injection mechanics (set, unset, bare
hook on stdin) stay out of lade --help; see
docs/protocol.md.
| Command | Role |
|---|---|
lade setup |
This repo: packages, lock, first-time pre-exec, pre-tool. --unlock, --harness. |
lade update |
Re-resolve implied and ranged pins; rewrite lock. |
lade teardown |
Remove repo pre-tool hooks; run ?teardown=. --global wipes on-disk cache. |
lade add / lade remove |
Edit nearest lade.yaml. add runs setup. Flags: --rule, --key, --uri. |
lade on / lade off |
Toggle pre-exec snippets for this shell. |
lade user |
Set per-user secret map key. --reset for OS default. |
lade approve <code> |
After a disclaimer, or prefix LADE_APPROVE=<code>. Exit 3 when withheld. |
lade eval <uri> |
Resolve one secret URI to stdout. |
lade bench |
Time parse, match, hydrate. Hidden from lade --help. --json, --timeout. |
lade upgrade |
Install newer lade and age-plugin-lade. --version, -y. |
lade status |
Version, hooks, mise, providers, hub counts. --json, --all. |
lade cache |
RAM key names for this binary. list, set 2h, unset, forget [KEY]. Never values. |
lade log / lade usage |
Diary queries. See docs/observability.md. |
lade mcp |
MCP bridge (below). |
lade -- <cmd> |
One-shot wrap (same as documented inject path). |
Progress tags: (c) cached from the hub, (o) overridden by a
closer yaml, (u) unset. Manual: docs/cli.md.
A vault edit does not evict RAM. lade cache lists names.
lade cache set 2h raises the 5m default for this cwd on the
hub. Parent folders share it. lade cache unset clears this
cwd. lade cache forget (or
lade log prune --hub) drops this binary's hub. Next hydrate
refetches. docs/cache.md.
silent: true on a rule skips secret progress lines for that rule.
Provider URIs can imply a locked CLI on setup (op:// → op,
vault:// → vault, …) even when the yaml has no mise:// pin.
lade status reports min CLI versions and sdk vs cli transport.
When a harness needs MCP rather than shell rewrite:
# stdio server
lade mcp -- npx -y @modelcontextprotocol/server-everything
# remote Streamable HTTP
lade mcp https://example.com/mcpBindings become upstream headers (HTTP) or env for the child (stdio).
Same rule matching, disclaimers, and diary as lade -- <cmd>. Details:
docs/architecture.md.
Build time only. Same yaml. Not production runtime.
curl -fsSL https://raw.githubusercontent.com/zifeo/lade/main/installer.sh | CI=1 bash
lade setupInstaller env: OUT_DIR, VERSION, PLATFORM, ASSUME_YES, CI,
DOWNLOADER (docs/env.md).
GitHub Action zifeo/lade (setup-lade): inputs version (default
latest), out-dir (default ${{ github.workspace }}/.lade-bin).
Installs lade and age-plugin-lade with cache.
Image: ghcr.io/zifeo/lade (linux musl, ENTRYPOINT is lade).
age-plugin-lade lets age
hydrate a secret through Lade. The plugin execs lade eval. Keep
lade next to it or on PATH.
cargo install age-plugin-lade --lockedFrom this repo the plugin is crates/age-plugin-lade:
cargo install --path crates/age-plugin-lade --lockedA command can build one value from another (${NAME}), run a
shell snippet (sh://), or pull a field from age or SOPS. Blueprint:
docs/architecture.md. T tickets and internal
env: docs/protocol.md,
docs/env.md. Provider examples:
examples/providers/lade.yml.
CLI manual (every public verb, examples, loader marks):
docs/cli.md. lade log and lade usage flags:
docs/observability.md.
CI that talks to 1Password without a person: set
1password_service_account on . (per-user map supported; see
examples).
Users: mise pins lade. The binary on PATH is the CLI and the
hub. Each image has its own socket, so several hubs can be up.
There is no idle exit. Logout, crash, or a new inode drops that
hub. Cache does not cross binaries.
Local work: cargo install --path . must beat every mise lade
on this machine. One global mise setting, once:
mise settings add disable_tools github:zifeo/ladewhich may still show the mise shim. That shim execs
~/.cargo/bin/lade. Do not set MISE_DISABLE_TOOLS in the
shell. mise reads this file at activate.
eval "$(lade off)"
eval "$(cargo run -- on)"
cargo test --workspace --locked
eval "$(cargo run -- off)"
eval "$(lade on)"







