---
name: awesomate-credentials
description: Safely capture and store API keys, tokens, database URLs and other secrets for the user's Awesomate apps — via a local secret-drop link so the value never enters the chat, encrypted in the hub and injected into the app's .env, never committed to git, never echoed back. Use whenever the user needs to provide a secret ("here's my API key", "I need to give you a key / token / password", "add this key", "store this secret", "save my credentials"), pastes something that looks like a secret, or drops a key into a file. Companion to awesomate-app-builder. If the secret is for an n8n workflow or automation, route to the awesomate-n8n skill instead — n8n credentials are created on the n8n instance, never in .env.
---

# Awesomate Credentials — handle secrets safely

The best moment to protect a secret is BEFORE it is typed. When the user needs
to provide one, don't ask them to paste it — serve a secret-drop link (below).
If they've already pasted it, store it fast and say the rotation line.

## Direct invocation (`/awesomate-credentials`)

When the user invokes this skill by name without revealing a secret, go
straight to the link flow — the link IS the answer, not a description of it.
If the conversation already says which app the key is for, launch hub mode for
that app now. Otherwise ask exactly one question — *"is this for one of your
apps, or just a key to keep on this machine?"* — then launch.

## The rules (non-negotiable)

1. **Never echo a secret value back** — not in a summary, not in a code block,
   not "just to confirm". Confirm with the **key name + a redacted preview
   only** (e.g. `OPENAI_API_KEY = sk-…redacted`).
2. **Never commit a secret.** App secrets live in `.env` (gitignored) or the
   hub's encrypted store — never in tracked files. If you see a secret in code
   about to be committed, stop and move it.
3. **A secret that entered the conversation is exposed** — pasted into chat OR
   read from a file into your context. Store it, then tell the user plainly:
   *"treat it as exposed — rotate it when convenient; next time I'll give you a
   secret-drop link so it never touches the chat."*
4. **The secret-drop link is the only zero-exposure flow.** Prefer it whenever
   the value hasn't been revealed yet.

## The secret-drop link (preferred)

A one-time form served on the user's own machine (binds 127.0.0.1 only). The
value goes browser → destination directly; you only ever see the key NAMES.

1. Pick a free port (45000–49000) and invent a 32-char hex token. Both may
   appear in chat — they gate the link, not the secret.
2. Start it in the background. Two destinations:

   **App runtime secret** (an API key the backend calls, a token — goes to the
   app's encrypted hub env, then its `.env`, then the app restarts):
   ```bash
   node ~/.claude/skills/awesomate-credentials/scripts/secret-drop.mjs \
     --hub-app <appId> --env dev --keys STRIPE_SECRET_KEY --port <port> --token <hex>
   ```
   Get `<appId>` from `awesomate_app_list`. Keys must be UPPER_SNAKE: the
   drop script uppercases what you pass it in hub mode, but the
   `awesomate_app_set_env` TOOL does not — it hard-rejects anything that
   isn't `^[A-Z][A-Z0-9_]{0,63}$`, so a lowercase name or a leading
   underscore is a validation error there, not a tidy-up. Pick the final
   uppercase name once and use it in both places. Node apps only — static
   sites have no server env (`set_env` returns 400 for them). The
   script posts each value to the hub with the user's own PAT
   (`~/.awesomate/credentials.json`) — the same route `awesomate_app_set_env`
   uses, so encryption, `.env` injection and restart all behave identically.

   **A personal key just to keep** (not an app runtime secret):
   ```bash
   node ~/.claude/skills/awesomate-credentials/scripts/secret-drop.mjs \
     --keys OPENAI_API_KEY --port <port> --token <hex>
   ```
   Writes to `~/.config/api-keys.env` (mode 600); `--file .env` targets a local
   dev env file instead (gitignored files only).

3. Give the user the link as clickable markdown —
   `http://127.0.0.1:<port>/?t=<token>` — and say: *"click this and paste your
   key there; I never see the value."*
4. The background task prints `SAVED: <names>` (names only, never values). In
   hub mode a failed key prints `FAILED: <name> (status …)` and the form stays
   up so the user can resubmit — report the reason and have them retry.
5. `EADDRINUSE` on start → relaunch on another port with a fresh token.
6. Confirm with key names only. No rotation warning needed — the value never
   entered the conversation.

### What the drop link can't do

- **The link is short-lived and single-use.** It expires after 15 minutes
  (`--ttl <minutes>` to change it) and the server exits after the FIRST
  successful save unless you passed `--stay`. If the user comes back later
  and the page is dead, that's expected — start a fresh one on a new port
  with a new token rather than telling them to retry the old link.
- **It cannot carry a MULTI-LINE value.** The bulk box is parsed line by
  line, splitting on the first `=`, so a PEM private key or a Google
  service-account JSON loses everything after line 1 — silently, and those
  are two of the commonest secrets people have. The named field is a
  single-line input, so it can't hold one either. For a multi-line secret,
  either:
  - **base64 it into one line** — `base64 -i key.pem | tr -d '\n'` — drop
    that through the link as e.g. `GOOGLE_SA_JSON_B64`, and decode it in the
    app at startup; or
  - have the user **save it to a file** on their machine and tell you the
    path, then read and store it — being honest that reading it puts the
    value in the conversation record, so the rotation nudge applies.

## Detect and offer

If the user's message contains something shaped like a secret — `sk-…`,
`ghp_`/`gho_…`, `AKIA…`, `xox[bp]-…`, a bearer token, a `postgres://…` /
`mysql://…` URL — it is already exposed: store it immediately (below), redact
it from your responses, and say the rotation line. But if they ANNOUNCE a
secret without pasting it ("I've got the Stripe key ready"), that is the moment
the link flow exists for.

## Where a secret goes

Read [references/where-secrets-go.md](references/where-secrets-go.md) for the
full map. Short version:

- **A running app needs it** (API key the backend calls, a DB URL) → the app's
  encrypted hub env + `.env` (0600) + restart. Via the secret-drop `--hub-app`
  link when the value hasn't been revealed, or **`awesomate_app_set_env`**
  (tool) when it already has. Ask which env (default `dev`). Node apps only —
  static sites have no server env.
- **A personal key just to keep** → `~/.config/api-keys.env` (chmod 600), via
  the file-mode link; add a rotation note if it came through the chat.
- **Never** a tracked file, a commit, or the chat.

A secret for an n8n workflow belongs on the n8n instance (awesomate-n8n
skill), never in an app `.env`. Those tools are consent-gated: if one comes
back 403 `consent_required`, call `awesomate_privacy_settings` — it names
the exact toggle that's off, so you can send the user to the right switch at
hub.awesomate.ai/settings?tab=privacy instead of guessing. You can't flip it
for them.

## Fallback: the file-drop flow

Only when the link flow can't work (e.g. Claude is running on a remote machine
the user's browser can't reach as 127.0.0.1):

1. Tell them: *"Create a file `.awesomate/secret-inbox.txt` in your project and
   paste the value there — I'll grab it and wipe the file."*
   **Verify `.gitignore` yourself before any commit.** Don't assume the
   folder is ignored: the github skill's setup script adds `.awesomate/` only
   when it has actually been run in this project, and older installed copies
   of that script didn't list it at all. `grep -q '^\.awesomate/' .gitignore`
   — add the line if it's missing, before the file exists, and never commit
   while it does.
2. Read the file, store via `awesomate_app_set_env`, then **scrub the file**:
   `node ~/.claude/skills/awesomate-credentials/scripts/capture-secret.mjs .awesomate/secret-inbox.txt`
3. Be honest with the user: reading the file put the value in the conversation
   record, so the rotation nudge still applies — softer than a chat paste, not
   zero.

## After storing

Tell the user what you set (key + env + where), that it's encrypted and
injected, and — if the value ever entered the conversation — the one-line
rotation nudge. Never restate the value.
