KASETTO

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):

{
  "mcpServers": {
    "vercel": {
      "url": "https://mcp.vercel.com",
      "type": "http",
      "headers": {
        "Authorization": "Bearer ${kst_vercel_token}"
      }
    }
  }
}

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.

Placeholdercredentials.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:

  1. Environment variables — the placeholder name as written, then uppercased, e.g. ${kst_vercel_token}kst_vercel_token, then KST_VERCEL_TOKEN.
  2. credentials.yaml$XDG_CONFIG_HOME/kasetto/credentials.yaml (~/.config/kasetto/credentials.yaml), plus any extra files listed in secrets.files.

Environment wins over the file, so one shared pack works in CI (env vars) and on a laptop (credentials.yaml) without editing.

# ~/.config/kasetto/credentials.yaml — never commit this
kst_github_token: ghp_xxx        # flat key
vercel:
  token: xxxxxxxx                # nested → ${kst_vercel__token}

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

${kst:env:VERCEL_TOKEN}

Looks up VERCEL_TOKEN verbatim (no kst_ prefix, no uppercasing) — point it at any env var you already export.

crd — a credentials.yaml path

${kst:crd:vercel/token}

Descends the /-separated path in credentials.yaml (vercel.token), case-insensitively. Unlike the chain form, this never falls back to the environment.

op — 1Password

${kst:op:<vault>/<item>/<field>}
${kst:op://<vault>/<item>/<field>}

Both forms run op read op://<vault>/<item>/<field> (the 1Password CLI). Example: ${kst:op:Private/GitHub/token}.

vault — HashiCorp Vault

${kst:vault:<kv-path>#<field>}

Runs vault kv get -field=<field> <kv-path> (the Vault CLI). Example: ${kst:vault:secret/myapp#token}.

kp — KeePass

${kst:kp:<entry>#<attr>}

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:

secrets:
  keepass:
    database: ~/secrets.kdbx
    # key_file: ~/secrets.keyx   # optional

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

${kst:aws:<secret-id>#<json-key>}

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

${kst:gcp:<name>#<json-key>}

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

${kst:az:<vault>/<name>#<json-key>}

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

${kst:pass:<path>}

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

${kst:keychain:<service>#<account>}

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.

kst sync --allow-missing-secrets

With --allow-missing-secrets, Kasetto warns instead and leaves the literal placeholder in place. The same policy can be set in config:

secrets:
  on_missing: warn   # error (default) | warn

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:

kst sync --update            # rotate everything
kst sync --update vercel     # rotate one server

--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.

KeyRequiredDescription
on_missingnoerror (default) — fail; or warn — leave the placeholder and continue.
filesnoExtra credential files (relative to the config dir, or absolute), searched after the default credentials.yaml.
keepassnoKeePass database for ${kst:kp:...} refs — database (path) and optional key_file. No password (that comes from KST_KEEPASS_PASSWORD).
secrets:
  on_missing: error
  files:
    - ./team-credentials.yaml
  keepass:
    database: ~/secrets.kdbx
    key_file: ~/secrets.keyx

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.