---
name: secrets
description: Manage the full own-project vault and granted org-infra secret names.
---

**Host-native invocation:** Claude `/mmi:secrets` · Codex `$mmi:secrets` · Cursor `/secrets` · jervcode `/secrets` · Hermes skill_view. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.

# /secrets — two-tier project secrets

**Authority:** a project-admin self-serves their own product repo's full vault tree (`mmi-cli oracle org access
role` → `project-admin` + `train`): stageless, `dev`, `rc`, and `main`. Do not redirect them to master for
an own-project coordinate. Only org-infra grants and cross-project administration stay master-only.

Secrets in the org split by **blast radius + who manages them** (not by storage — both tiers are SSM
SecureString + KMS, and **a value is never echoed to chat or logs**):

- **YOUR REPO** — `/mmi-future/<slug>/<KEY>` (the stageless canonical) plus
  `/mmi-future/<slug>/{dev,rc,main}/*` (staged per-stage overrides) for a repo you are project-admin of.
  You **self-serve your own repo's full tree** — the canonical root, dev, rc, AND main prod secrets — via
  `/secrets`, on your **GitHub role alone**, no AWS, no waiting on the master (#2032).
- **ORG-INFRA** — the shared/infra namespaces (`/mmi-future/{shared,cloudflare,docs,mmi-hub}/*`), at
  **every** tier (their `dev/` holds Hetzner/AWS/CF tokens + Hub tooling). **Crown jewels, master-only.**
  A project-admin reaches one of these only via a master **grant**; the master is unrestricted (master ⊇
  project-admin).

**Grant scope (#3652).** A grant is `rw` by default — it opens both keyless `use` and `set`/`rm` on that
key. `mmi-cli vault secrets grant <repo> <login> <key> --read` grants **consume-only** reach: keyless `use`
works, rotate and remove still refuse. Only a `--read` grant may name a **wildcard** key — `*` for the
whole namespace, `<provider>/*` for one provider group — so "may read every crown jewel" is one grant
instead of forty. A wildcard never confers write; asking for one as `rw` is refused, not narrowed.
`mmi-cli oracle org access capabilities` prints `grant (read)` or `grant (rw)` next to each name.

A bare `<KEY>` is the **stageless canonical** at the slug root — one value every stage shares (#2244)
and the path deploys resolve (#2523). A staged `dev/<KEY>` is a **per-stage override** and WINS for that
stage at deploy (#2522); `set`/`use` warn (names only) when an override shadows the canonical. The
`dev`/non-`dev` `tier` field in older JSON is compatibility metadata, not an authority boundary: an
own-project `main/SECRET_KEY_BASE` or `rc/DB_URL` remains project-admin self-service.

**Declare-first (#2528):** the registry catalog is the SSOT; SSM is a projection of it. A `set` to a
coordinate not declared in the catalog is **rejected** — the error names the attempted path and the fix.
Declare the key first via `mmi-cli oracle org project set <owner/repo> --secrets-file <catalog.json>` (`stages: []`
= the shared stageless canonical, `stages: ["dev"]` = a per-stage override), then retry. See what is
declared with `mmi-cli vault secrets catalog` or `secrets list`; ask for a key you lack via `mmi-cli vault secrets
request <KEY>`. There is no master bypass. `rm` of an undeclared path stays allowed, so cleanup keeps
working. Google OAuth is one stageless pair per repo: bare `GOOGLE_CLIENT_ID` + `GOOGLE_CLIENT_SECRET`
at the slug root (the staged `{dev,rc,main}/GOOGLE_*` shape is retired; `org oauth set-creds` writes the
bare keys).

All ops run through `mmi-cli vault secrets …`, which calls the org backend with the caller's `gh` token; the
backend re-verifies **project-admin-of-this-repo** and the **vault namespace** server-side and does the scoped
SSM op. This skill never touches AWS.

## Step 0 — orient

Read the current repo + slug; the tier of a key follows from its name (above). `secrets list` shows what
exists and which ones **you** can manage (a `*`), never values.

```bash
mmi-cli vault secrets list                 # names + tier + a * on the ones you can write. NEVER values.
```

`secrets: no registry META` or `Hub API unreachable` → the repo is not registered with the Hub, GitHub auth
is missing, or the Hub API is unavailable. Run `mmi-cli oracle org project get <owner/repo>` to distinguish those cases;
a master-admin backfills the registry before secrets can be resolved.

## Step 1 — the verb

Default to the **current repo**; pass `--repo owner/Name` to target another (you must be its project-admin).

```bash
# CONSUME a secret keyless — injected into the command's env, NEVER printed (raw `secrets get` was
# removed, the floor wins, #2844):
mmi-cli vault secrets use SCRAPER_API_KEY -- <cmd>

# Write / rotate — the VALUE is read from stdin, NEVER an argument (so it can't leak into shell history
# or process args). Pipe it in:
printf %s "$THE_VALUE" | mmi-cli vault secrets set SCRAPER_API_KEY

# Validate a known provider key without printing its value:
mmi-cli vault secrets verify RECALL_API_KEY

# Remove:
mmi-cli vault secrets rm SCRAPER_API_KEY
```

**Never** pass a value as an argument (`secrets set KEY thevalue` is wrong — there is no value arg). The
confirmation prints the **name and tier only**, never the value.

**Windows: a multiline wrapped command must be file-backed (#6048).** `cmd.exe` truncates at the first
newline, so an inline `node -e "<multiline script>"` after `--` reaches the child truncated and fails
before your code runs. Write the script to a **project-local ignored file** (e.g. `tmp/fetch-models.mjs`
— git-ignored, never tracked) and wrap the file instead; the key is still injected into the child's env:

```bash
mmi-cli vault secrets use GEMINI_API_KEY -- node tmp/fetch-models.mjs
```

## Rotation checklist

Before rotating, enumerate every copy of the key so no tier stays stale:

```bash
mmi-cli vault secrets list --repo owner/Repo
```

Check the bare canonical plus any `dev/`, `rc/`, `main/` overrides the list shows. Rotate the provider-side
key first, then write every declared coordinate that still needs that key. For keys with a provider probe
(`RECALL_API_KEY` today), `secrets set` validates the new value before printing success; `secrets verify
<KEY>` repeats the same probe later without printing the value. If the verifier fails, treat the rotation as
incomplete even if the vault write itself succeeded.

## Step 2 — org-infra elevation (master-only)

Your own repo's full tree (dev/rc/main) is already self-serve (#2032). The **org-infra** namespaces
(`shared`/`cloudflare`/`docs`/`mmi-hub`, every tier) stay master-only. A project-admin who needs one of
those keys asks the master, who grants a **scoped, auditable** standing access to that one key (or revokes
it). These verbs are **master-only** — the backend 403s anyone else.

```bash
# MASTER: let @oguz-mut consume one _org provider key from their MM-Chat project
mmi-cli vault secrets grant mutmutco/MM-Chat oguz-mut google/SOME_SHARED_KEY
# MASTER: withdraw it
mmi-cli vault secrets revoke mutmutco/MM-Chat oguz-mut google/SOME_SHARED_KEY
```

The master can also operate the org-infra key directly while guiding (master ⊇ project-admin). Org-infra
access by a project-admin **always originates from the master** — own-project access does not.

## Coordinate — where does a new project secret belong?

- Shared across stages → bare `<KEY>` at the project slug root (the usual canonical value).
- Genuinely stage-distinct → `<stage>/<KEY>` override for `dev`, `rc`, or `main`.
- The project-admin owns both shapes across their own project. A path under `_org`/shared infrastructure is
  not a project coordinate and stays master-managed.

## Tier-to-tier copy (provider keys, #1433)

**Encryption / stage-distinct keys** (`*_ENC_KEY`, `SECRET_KEY_BASE`, etc.) must be **generated per stage** —
never copied. **Provider sandbox keys** (e.g. Recall/Gemini for Katip dev/rc) **may** be shared across dev/rc
when they point at the same sandbox project; prod should stay distinct when the provider contract requires it.

Sanctioned copy within the own-project vault (org-infra/cross-project sources remain master-gated):
```bash
mmi-cli vault secrets copy --from rc --to dev --keys RECALL_API_KEY,GEMINI_API_KEY
mmi-cli vault secrets copy --from rc --to dev --keys RECALL_API_KEY --dry-run   # plan only
```
Prefer this over manual copy piping — audit-logged, blocklist enforced.

## Notes

- **Never echo a value** — not in chat, a commit, a log, or an issue. No command emits a value to stdout;
  consume one keyless with `secrets use <KEY> -- <cmd>` (#2844). Treat every secret as write-once.
- Authority is **GitHub**: project-admin = repo `admin`, or repo `write`/`maintain` plus the registry `projectAdmins` entry; master =
  org owner. The backend checks both with the org App token, so the decision is server-side (your token
  scope can't widen it).
- Runtime consumes declared keys through its scoped broker token (and central deploy injects the declared
  stage set without a repo `.env`); CI reads keylessly through its OIDC role. Neither uses raw `/secrets`
  output. `secrets use <KEY>` is the safe local command-consumption path.
- Every self-service write is **attributable** — the backend logs actor + repo + KEY name (never the value).

## Retro — one check before you finish
Before your final report, answer one question honestly: did **this skill's own instructions** misfire
this run — ambiguous wording, a misleading message, or an environment failure it should have warned
about? (Process only — never the user's code or task; e.g. a canonical-name or tier mix-up, or a step
that risked echoing a value.) If yes, file **one** lesson and move on; a clean run is silent (hard cap:
one per run). It lands on the owning board (deduped) and is fixed only via a reviewed PR — never edit the
skill live; the retro is advisory, so if the call fails, note it and continue. File through MMI:
`mmi-cli learning skill-lesson --skill secrets --title "<what misfired>" --body "<what; evidence; proposed amendment>"`
