Secret Injection
Inject tokens into MCP configs at sync time with ${kst_...} placeholders — never committed, never in the lock.
When you need this: an MCP server needs a token, password, or API key, and you don't want to commit it into the pack you share.
What you'll learn:
- How
${kst_...}placeholders are written and resolved - Where values come from, and how to pin a specific source
- What happens when a secret is missing, and how rotation works
Placeholders
Reference a secret anywhere inside an MCP server definition — a header, an env value, a URL. At sync time Kasetto resolves it and writes the value into the agent's settings file. There are two forms.
The chain form ${kst_<name>} resolves against an ordered fallback (env, then credentials):
The lowercase kst_ (or kst:) sentinel is the opt-in. A bare ${HOME} or ${PATH} is left untouched, so placeholders the agent or shell expands at launch time pass straight through — Kasetto only resolves its own sentinel.
Nested keys
In the chain form, a double underscore (__) descends into a nested credentials path. A single underscore stays within one segment.
| Placeholder | credentials.yaml path |
|---|---|
${kst_vercel_token} | kst_vercel_token (flat) |
${kst_vercel__token} | vercel.token (nested) |
${kst_org__ci__token} | org.ci.token (nested) |
Keys match case-insensitively, so vercel.token resolves ${kst_vercel__token}, and the chain also tries the env var uppercased — ${kst_vercel_token} reads KST_VERCEL_TOKEN from the environment.
The chain (env → credentials)
The chain form resolves against an ordered set of sources — the first that has the value wins:
- Environment variables — the placeholder name as written, then uppercased, e.g.
${kst_vercel_token}→kst_vercel_token, thenKST_VERCEL_TOKEN. credentials.yaml—$XDG_CONFIG_HOME/kasetto/credentials.yaml(~/.config/kasetto/credentials.yaml), plus any extra files listed insecrets.files.
Environment wins over the file, so one shared pack works in CI (env vars) and on a laptop (credentials.yaml) without editing.
credentials.yaml holds plaintext secrets. Keep it out of version control and lock it down with chmod 600 — Kasetto warns when the file is group- or world-readable.
Pinning a source
When you want to bypass the fallback and name exactly one source, use the tagged form ${kst:<source>:<ref>}. Each tag pins exactly one provider.
env — a specific environment variable
Looks up VERCEL_TOKEN verbatim (no kst_ prefix, no uppercasing) — point it at any env var you already export.
crd — a credentials.yaml path
Descends the /-separated path in credentials.yaml (vercel.token), case-insensitively. Unlike the chain form, this never falls back to the environment.
op — 1Password
Both forms run op read op://<vault>/<item>/<field> (the 1Password CLI). Example: ${kst:op:Private/GitHub/token}.
vault — HashiCorp Vault
Runs vault kv get -field=<field> <kv-path> (the Vault CLI). Example: ${kst:vault:secret/myapp#token}.
kp — KeePass
Runs keepassxc-cli show -s -a <attr> <database> <entry> (the KeePassXC CLI). The attribute defaults to Password when #<attr> is omitted. Example: ${kst:kp:GitHub/PAT} or ${kst:kp:GitHub/PAT#Token}.
Unlike op/vault, KeePass has no ambient session, so it needs the database location in config:
The database is unlocked with a key-file (secrets.keepass.key_file, if set) and/or a master password read from the KST_KEEPASS_PASSWORD environment variable. With neither, kst adds --no-password and closes stdin, so the CLI fails fast instead of waiting on a prompt.
aws — AWS Secrets Manager
Runs aws secretsmanager get-secret-value (the AWS CLI) against your active profile/region. AWS secrets are often JSON, so an optional #<json-key> extracts a top-level field. Example: ${kst:aws:prod/db} or ${kst:aws:prod/db#password}.
gcp — Google Cloud Secret Manager
Runs gcloud secrets versions access latest --secret=<name> (the gcloud CLI) against the active project. An optional #<json-key> extracts a top-level field when the secret is a JSON document. Example: ${kst:gcp:db-password} or ${kst:gcp:db#password}.
az — Azure Key Vault
Runs az keyvault secret show --vault-name <vault> --name <name> (the Azure CLI). An optional #<json-key> extracts a top-level field when the secret is a JSON document. Example: ${kst:az:my-vault/db-password} or ${kst:az:my-vault/db#password}.
pass — pass / gopass
Runs pass show <path> (the pass password store; gopass is compatible) and returns the first line — the password, by the store's convention. Example: ${kst:pass:work/vercel}.
keychain — macOS Keychain
Runs security find-generic-password -s <service> [-a <account>] -w against the macOS Keychain; the #<account> is optional. Example: ${kst:keychain:vercel-token}.
op, vault, kp, aws, gcp, az, pass, and keychain all shell out to the provider's own CLI at sync time, inheriting whatever session and auth you already have — no tokens in any kasetto file. If the CLI is missing or the lookup fails (item not found, not authenticated, locked database), the entry is a hard failure with the CLI's error, regardless of --allow-missing-secrets.
Missing Secrets
By default a missing required secret is a hard failure: the MCP entry is marked broken, nothing is written for it, and kst sync exits non-zero. Other assets still sync.
With --allow-missing-secrets, Kasetto warns instead and leaves the literal placeholder in place. The same policy can be set in config:
Rotation
Rotating a secret requires --update. A plain kst sync will report the server unchanged and leave the old value in place — it does not re-resolve secrets on its own. When a secret-bearing server is skipped, kasetto prints a tip reminding you to run --update.
A plain kst sync is stable: an MCP server already present in the agent's settings is left exactly as is, so a rotated secret in credentials.yaml or the environment does not propagate on its own. This is the same zero-churn guarantee the lock provides everywhere else.
To push a rotated secret, run an update — it re-resolves and replaces the managed server block:
--update overwrites only the servers Kasetto installed. Servers you added by hand, and your edits to other entries, are never touched.
What Never Gets Stored
Injection happens in memory, only on the value written to the agent destination. The plaintext secret never reaches:
kasetto.lock— the lock hashes the placeholder source file, not the resolved value.- The source cache and stage directory — they hold the pack exactly as authored.
So kasetto.lock stays commit-safe even when the synced MCP config carries a live token.
Config Reference
The secrets: block is optional and carries no values — it is safe to commit.
| Key | Required | Description |
|---|---|---|
on_missing | no | error (default) — fail; or warn — leave the placeholder and continue. |
files | no | Extra credential files (relative to the config dir, or absolute), searched after the default credentials.yaml. |
keepass | no | KeePass database for ${kst:kp:...} refs — database (path) and optional key_file. No password (that comes from KST_KEEPASS_PASSWORD). |
Secret injection currently applies to MCP configs. An unknown tag (anything other than env, crd, op, vault, kp, aws, gcp, az, pass, or keychain) errors clearly rather than silently passing through.