← All Skills

snappy-settings

v1.0.0
52 files, 434.4 KB ~13,556 words · 55 min read Updated 2026-09-09

snappy-settings skill

42 of 47 checks pass
What it can do
check keyread
listread
What does not pass yet
$ npx snappy-skills install snappy-settings
zip ↓
File Tree
├── AGENTS.md ├── SKILL.md ├── agents-md.test.ts ├── agents-md.ts ├── api.ts ├── contract-openapi.ts ├── docs/ │ ├── consolidation-candidates.md │ ├── consolidation-decisions-needed-2026-04-11.md │ ├── snappy-system-review.md │ └── snappy-system.md ├── env-reads.test.ts ├── env-reads.ts ├── evidence-additive.gate.test.ts ├── evidence-envelope.test.ts ├── evidence-envelope.ts ├── google-token.ts ├── hand-read.ts ├── hand-resources.test.ts ├── hand-resources.ts ├── load.ts ├── master-key.test.ts ├── master-key.ts ├── profile.ts ├── providers-choice.test.ts ├── providers-choice.ts ├── read-answer.test.ts ├── read-answer.ts ├── read-limit.test.ts ├── read-limit.ts ├── refusal-codes.test.ts ├── refusal-codes.ts ├── scripts/ │ ├── agents-md-findings.mjs │ ├── bootstrap.sh │ ├── cert-check.sh │ ├── check-creds.sh │ ├── dry-check.sh │ ├── generate-skill-index.sh │ ├── get-cred.sh │ ├── load-env.sh │ ├── regenerate-skill-indices.sh │ ├── skill-check.sh │ └── sync-from-xano.ts ├── skill-spec.md ├── spawn-pool.test.ts ├── spawn-pool.ts ├── spec-read.test.ts ├── spec-read.ts ├── stage.ts ├── tool-annotations.test.ts ├── tool-annotations.ts ├── vendor-fetch.test.ts └── vendor-fetch.ts
Documents
AGENTS.md

snappy-settings -- Agent Loader#

You are the credential layer for Snappy. Every snappy-* skill that hits an external API sources its env vars from here. Credentials live in one file: .env.cache. That file is the single source of truth. No Bitwarden, no cloud sync. Edit it directly.

API module#

typescriptimport { env, loadAll, listCredentials, checkCredential } from "../snappy-settings/api.ts";

Or CLI:

bashnpx tsx ~/.claude/skills/snappy-settings/api.ts list              # show all credential key names (NOT values)
npx tsx ~/.claude/skills/snappy-settings/api.ts check SLACK_USER_TOKEN  # check if a credential is present

API functions#

Function Purpose
env(key, required?) Read a credential by key; throws if missing (unless required=false, then returns "")
loadAll() Returns flat object of all credentials from .env.cache
listCredentials() Returns sorted array of all credential key names (never exposes values)
checkCredential(key) Returns boolean indicating whether a credential is present and non-empty

TypeScript API (primary)#

typescriptimport { env, loadAll } from "../snappy-settings/load.ts";

const token = env("SLACK_BOT_TOKEN");          // throws if missing
const key   = env("OPENAI_API_KEY", false);    // returns "" if missing
const all   = loadAll();                       // flat object of all credentials

load.ts reads .env.cache once, caches in memory, zero-config. Works in subagents and cron.

Bash helpers (legacy skills with scripts/*.sh)#

bashsource ~/.claude/skills/snappy-settings/scripts/load-env.sh         # export every key
KEY=$(~/.claude/skills/snappy-settings/scripts/get-cred.sh OPENAI_API_KEY)
~/.claude/skills/snappy-settings/scripts/check-creds.sh             # audit which keys are set

All three scripts just read .env.cache. No Bitwarden.

The stage road -- stage.ts (NO QUEUE)#

typescriptimport { stageHandOperation } from "../snappy-settings/stage.ts";

One helper, two answers, no third. Every write verb in every hand calls it

before it touches the world; no hand carries a copy of the rule.

Condition What happens
A daemon answers GET /healthz within 1 s (at SNAPPY_RENDER_BASE_URL / SNAPPY_DAEMON_URL / SNAPPY_HEAD_SCREEN_URL, else http://127.0.0.1:3147) POST /hands/stage with a 15 s ceiling; the hand prints staged for approval: control <id>. Unchanged -- the app road.
No daemon answers A typed preview is printed as JSON on stdout, exit 0. Nothing staged, nothing sent.
json{ "staged": false, "mode": "preview", "skill": "snappy-linkedin", "verb": "post",
  "args": { "text": "..." },
  "preview": "Publish a LinkedIn post — linkedin\nnot reversible · risk high · content\ntext: ...",
  "run_with": "npx tsx ~/.claude/skills/snappy-linkedin/api.ts post '...' --now",
  "reason": "daemon_not_configured", "detail": "..." }

What you do with a preview: show preview to the owner IN THIS

CONVERSATION and wait for his word. On his word, run run_with verbatim -- it

is the same command with --now. Never pass --now on your own initiative;

--now is the owner's yes, not yours.

Classify it by its discriminants -- staged === false && mode === "preview"

-- never by scanning the output for words. Exit 0 is deliberate and overrides

skill-spec §1b.7: a preview is not a failure, and exit 1 made runners throw it

away as a crash.

Why (the owner, 2026-09-08 18:4x-18:5x, binding): *"it is the work done in

real time; forget Needs You; stage means show it to me and wait for my word in

this conversation, never a row in a table."* Measured the same day at 21:2x: the

app daemons were stopped on both Macs and every write verb died --

snappy-linkedin post exited 1 with

not staged: {"error":"stage_door_unreachable"...} -- while the owner sat at the

keyboard ready to say yes.

Importers, measured 2026-09-08 (grep -l "snappy-settings/stage.ts" skills/*/api.ts), 8:

snappy-freshbooks, snappy-gmail, snappy-hands, snappy-imessage,

snappy-linkedin, snappy-skool, snappy-slack, snappy-telegram.

The provider choices -- providers-choice.ts (THE ONE READER)#

⟨owner, 2026-09-09 10:25, on the bar's Providers tab⟩ *"I choose one for

default, I choose the fallback, I choose the level being used for each one …

it should all be controllable and saveable there — a big asset."*

The bar saves those three choices to ~/.snappy-skills/providers.json:

json{ "default": "chatgpt",
  "fallback": ["openrouter", "claude"],
  "effort": { "chatgpt": "xhigh", "claude": "low" },
  "at": "2026-09-09T14:22:31.004Z" }

**providers-choice.ts is the kernel's ONE reader of that document. Never

write a second parser of it, anywhere.** The file has exactly one writer —

writeChoices() in snappy-runner/src/providers.ts — and the two repos cannot

share a module, so the contract is held by citation: every field filter mirrors

that file's readChoices(), and the four effort words are its EFFORTS. This

reader never writes, never repairs, and never performs the runner's

~/.snappy-runner~/.snappy-skills rename.

typescriptimport { providerChoices, defaultProvider, fallbackOrder, providerOrder, effortFor }
  from "../snappy-settings/providers-choice.ts";
Function Answers
providerChoices() The whole document, re-read per call, plus source: "document" / "absent" / "unreadable"
defaultProvider() The provider he reaches for first, or null if he never chose
fallbackOrder() His fallback list exactly as saved
providerOrder() The one ordering to walk: default first, then the fallback, each id once
effortFor(id) His level for one provider, or nullnull is "no preference", never "medium"
providersChoicePath() The path, honouring SNAPPY_SKILLS_HOME (read per call, so a test can point it at a scratch dir)

The ids are the runner's, minted from jcode usage --json: chatgpt,

openrouter, claude, openai-api, gemini. They are NOT jcode's -p

words and NOT the model aliases; a skill that spends a provider owns the map

from an id to its own runner's name and says which map it used.

An absent or unparseable document answers the defaults and says so — it

never throws and never rewrites the file. Unknown effort words are dropped by

name; the known ones beside them survive.

Who reads it (measured 2026-09-09): snappy-jcode (provider + per-run

effort via PROVIDER_ROADS), snappy-shell (auto cascade order via

cascadeAliases()), snappy-dispatch (the alias an unasked errand spends via

defaultAlias()).

The credential file#

~/.claude/skills/snappy-settings/.env.cache    # chmod 600, plain KEY=value

Edit it with any editor. Lines starting with # are comments. Empty values (KEY=) are treated as unset -- callers should handle missing credentials explicitly.

Credential catalog#

Canonical status (updated 2026-04-08 after Bitwarden removal + bulk paste).

Xano#

Env var Used by
XANO Base URL https://xnwv-v1z6-dvnr.n7c.xano.io
XANO_METADATA_TOKEN snappy-knowledge, snappy-pipeline, snappy-infra metadata ops (single Xano auth token)

AI / LLM providers#

Env var Used by
OPENAI_API_KEY snappy-ai-models, snappy-image (DALL-E), snappy-video (Whisper)
OPENAI_ASSISTANT_ID snappy-ai-models (legacy assistants)
ANTHROPIC_API_KEY intentionally empty -- Claude Code subagents use parent session. Only set if a skill needs direct SDK calls.
GEMINI_API_KEY snappy-gemini, snappy-image (Nano Banana / Imagen)
OPENROUTER_API_KEY snappy-openrouter -- direct-API fallback only
REPLICATE_API_TOKEN snappy-image, snappy-video

Media generation (image / video / audio)#

Env var Used by
COMFYICU_API_KEY snappy-image (ComfyICU workflows)
FAL_API_KEY snappy-image, snappy-video (fal.ai)
SEGMIND_API_KEY snappy-image (Segmind)
ELEVENLABS_API_KEY snappy-video (voiceover)
DEEPGRAM_API_KEY snappy-video, snappy-transcripts

Messaging#

Env var Used by
SLACK_USER_TOKEN snappy-slack (primary, xoxp-)
SLACK_BOT_TOKEN snappy-slack (fallback, xoxb-)
TELEGRAM_BOT_TOKEN snappy-telegram, snappy-clients
TELEGRAM_ROBERT_CHAT_ID snappy-telegram (Robert's DM chat id)
WHATSAPP_TOKEN snappy-whatsapp (Meta Cloud API access token) -- empty
WHATSAPP_PHONE_ID snappy-whatsapp (phone number ID) -- empty
ROBERT_PHONE snappy-whatsapp (Robert's E.164 phone) -- empty

Docs / content distribution#

Env var Used by
NOTION_TOKEN snappy-docs, snappy-notion
TYPEFULLY_API_KEY snappy-linkedin (scheduled posting)
LATE_API_KEY Late.so social scheduling
LOOPS_API_KEY snappy-email (Loops.so transactional)

LinkedIn (OAuth)#

Env var Used by
LINKEDIN_CLIENT_ID snappy-linkedin OAuth flow
LINKEDIN_CLIENT_SECRET snappy-linkedin OAuth flow
LINKEDIN_ACCESS_TOKEN snappy-linkedin posting -- empty, regenerate via OAuth flow
LINKEDIN_AUTH snappy-linkedin legacy cookie state -- empty

YouTube (OAuth)#

Env var Used by
YOUTUBE_CLIENT_ID snappy-youtube OAuth flow
YOUTUBE_CLIENT_SECRET snappy-youtube OAuth flow
YOUTUBE_ACCESS_TOKEN snappy-youtube write ops -- empty, regenerate via OAuth flow

Billing#

Env var Used by
STRIPE_SECRET_KEY snappy-analytics (revenue), live key

FreshBooks -- intentionally empty until trust rebuilt#

Env var Used by
FRESHBOOKS_CLIENT_ID snappy-freshbooks -- DO NOT FILL until Robert authorizes
FRESHBOOKS_CLIENT_SECRET snappy-freshbooks -- DO NOT FILL
FRESHBOOKS_REFRESH_TOKEN snappy-freshbooks -- DO NOT FILL
FRESHBOOKS_ACCOUNT_ID snappy-freshbooks -- DO NOT FILL

Zoom (OAuth)#

Env var Used by
ZOOM_ACCOUNT_ID snappy-scheduling (meeting creation)
ZOOM_CLIENT_ID snappy-scheduling OAuth
ZOOM_CLIENT_SECRET snappy-scheduling OAuth
ZOOM_SECRET_TOKEN snappy-scheduling webhook verification

Infrastructure#

Env var Used by
GITHUB_TOKEN snappy-github, snappy-publish (primary PAT)
GITHUB_PAT snappy-github (secondary PAT, for dev-only repos)
VERCEL_TOKEN snappy-publish, snappy-website -- currently placeholder password, needs real token
CLOUDFLARE_API_TOKEN snappy-infra, snappy-gateway
CLOUDFLARE_ACCOUNT_ID snappy-infra, snappy-gateway
DO_SPACES_KEY DigitalOcean Spaces access
DO_SPACES_SECRET DigitalOcean Spaces secret
DO_SPACES_BUCKET snappy-image CDN bucket (robert-storage)
DO_SPACES_REGION tor1
DO_SPACES_ENDPOINT https://tor1.digitaloceanspaces.com

Browser automation#

Env var Used by
BROWSERBASE_API_KEY snappy-browse cloud browsers
BROWSERBASE_PROJECT_ID snappy-browse cloud browsers

Google Service Account (calendar, drive, gmail, sheets, docs)#

Env var Used by
GOOGLE_SERVICE_ACCOUNT_EMAIL snappy-calendar, snappy-email, snappy-docs
GOOGLE_SERVICE_ACCOUNT_KEY PEM with literal \n -- decode via .replace(/\n/g, '\n') before passing to googleapis
GOOGLE_CLIENT_ID snappy-email, snappy-inbox-sweep (OAuth client for personal Gmail account)
GOOGLE_CLIENT_SECRET snappy-email, snappy-inbox-sweep (OAuth client secret for personal Gmail account)

Agent infrastructure (internal)#

Env var Used by
OPENCLAW_GATEWAY_URL snappy-infra (Mac Mini tailscale gateway)
OPENCLAW_GATEWAY_TOKEN snappy-infra auth
AGENT_EVENT_TOKEN snappy-ops event bus auth
FALKORDB_API_KEY snappy-knowledge graph DB
SNAPPY_MASTER_KEY snappy-email, snappy-inbox-sweep (master symmetric key for local credential encryption)

All skills call their service APIs directly. Xano is only used where it IS the database (snappy-knowledge, snappy-pipeline).

Rules#

  • NEVER hardcode a token in a script, SKILL.md, AGENTS.md, or anywhere else. Always use env("KEY") or $KEY.
  • NEVER paste a token into a prompt or echo it to logs. Use length checks: ${OPENAI_API_KEY:+set}.
  • NEVER commit .env.cache.
  • NEVER pass a key as a CLI arg -- it leaks via ps aux. Env vars only.
  • NEVER reintroduce Bitwarden or another sync layer. That is the exact loop that wiped credentials on 2026-04-08. The file is the source of truth. Anything that rewrites .env.cache from elsewhere will destroy working data.

Adding a new credential#

  1. Add NEW_KEY=value to .env.cache
  2. Read it: env("NEW_KEY") from a skill's api.ts (or $NEW_KEY after sourcing load-env.sh)
  3. Add a row to the catalog table above

Three steps. Nothing to regenerate.

Failure modes#

  • .env.cache doesn't exist → STOP, tell the user to create it. Don't fabricate credentials.
  • A required key is empty → STOP, tell the user which key is missing and which skill needs it. Don't silently fall back.
  • Unknown key name → STOP, run check-creds.sh and list what's actually in the file.

Uses#

Every snappy-* skill that hits an external API reads from here via env(). The credential catalog above is the full list.


If this loader doesn't cover your case, read SKILL.md. If that still doesn't cover it:

bashecho "[$(date -u +%FT%TZ)] snappy-settings: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log

<!-- SKILL-INDEX-START -->

[snappy-settings Index]|root: ~/.claude/skills/snappy-settings|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,skill-spec.md}|docs:{consolidation-candidates.md,consolidation-decisions-needed-2026-04-11.md,snappy-system-review.md,snappy-system.md}

<!-- SKILL-INDEX-END -->

Used by#

  • snappy-ffmpeg
  • snappy-image
  • snappy-inbox-sweep
  • snappy-openrouter
  • snappy-os-operator
  • snappy-skill
  • snappy-sync

<!-- SNAPPY-CONTRACT-VERBS-START -->

Contract verbs#

Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.

Verb Contract arguments Effect First call
check key read npx tsx ~/.claude/skills/snappy-settings/api.ts check <key>
list read npx tsx ~/.claude/skills/snappy-settings/api.ts list

Show the result#

When an answer carries face_hint, show it with one snappy_present(<answer>) call.

See /snappy-faces for face selection. Human-facing images must crop to the

element, render at 2x on Retina, and fill the destination channel instead of

placing a small card in a full-page screenshot.

<!-- SNAPPY-CONTRACT-VERBS-END -->

---
name: snappy-settings
role: Credential and environment loader for all snappy-* skills
loaded-by: preload-skill-context hook
---

# snappy-settings -- Agent Loader

You are the credential layer for Snappy. Every snappy-* skill that hits an external API sources its env vars from here. **Credentials live in one file: `.env.cache`.** That file is the single source of truth. No Bitwarden, no cloud sync. Edit it directly.

## API module

```typescript
import { env, loadAll, listCredentials, checkCredential } from "../snappy-settings/api.ts";
```

Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-settings/api.ts list              # show all credential key names (NOT values)
npx tsx ~/.claude/skills/snappy-settings/api.ts check SLACK_USER_TOKEN  # check if a credential is present
```

## API functions

| Function | Purpose |
|----------|---------|
| `env(key, required?)` | Read a credential by key; throws if missing (unless `required=false`, then returns "") |
| `loadAll()` | Returns flat object of all credentials from .env.cache |
| `listCredentials()` | Returns sorted array of all credential key names (never exposes values) |
| `checkCredential(key)` | Returns boolean indicating whether a credential is present and non-empty |

### TypeScript API (primary)

```typescript
import { env, loadAll } from "../snappy-settings/load.ts";

const token = env("SLACK_BOT_TOKEN");          // throws if missing
const key   = env("OPENAI_API_KEY", false);    // returns "" if missing
const all   = loadAll();                       // flat object of all credentials
```

`load.ts` reads `.env.cache` once, caches in memory, zero-config. Works in subagents and cron.

## Bash helpers (legacy skills with `scripts/*.sh`)

```bash
source ~/.claude/skills/snappy-settings/scripts/load-env.sh         # export every key
KEY=$(~/.claude/skills/snappy-settings/scripts/get-cred.sh OPENAI_API_KEY)
~/.claude/skills/snappy-settings/scripts/check-creds.sh             # audit which keys are set
```

All three scripts just read `.env.cache`. No Bitwarden.

## The stage road -- `stage.ts` (NO QUEUE)

```typescript
import { stageHandOperation } from "../snappy-settings/stage.ts";
```

One helper, two answers, no third. Every write verb in every hand calls it
before it touches the world; no hand carries a copy of the rule.

| Condition | What happens |
|-----------|--------------|
| A daemon answers `GET /healthz` within **1 s** (at `SNAPPY_RENDER_BASE_URL` / `SNAPPY_DAEMON_URL` / `SNAPPY_HEAD_SCREEN_URL`, else `http://127.0.0.1:3147`) | `POST /hands/stage` with a 15 s ceiling; the hand prints `staged for approval: control <id>`. **Unchanged -- the app road.** |
| No daemon answers | A typed **preview** is printed as JSON on stdout, **exit 0**. Nothing staged, nothing sent. |

```json
{ "staged": false, "mode": "preview", "skill": "snappy-linkedin", "verb": "post",
  "args": { "text": "..." },
  "preview": "Publish a LinkedIn post — linkedin\nnot reversible · risk high · content\ntext: ...",
  "run_with": "npx tsx ~/.claude/skills/snappy-linkedin/api.ts post '...' --now",
  "reason": "daemon_not_configured", "detail": "..." }
```

**What you do with a preview:** show `preview` to the owner IN THIS
CONVERSATION and wait for his word. On his word, run `run_with` verbatim -- it
is the same command with `--now`. Never pass `--now` on your own initiative;
`--now` is the owner's yes, not yours.

**Classify it by its discriminants** -- `staged === false && mode === "preview"`
-- never by scanning the output for words. Exit 0 is deliberate and overrides
skill-spec §1b.7: a preview is not a failure, and exit 1 made runners throw it
away as a crash.

**Why** (the owner, 2026-09-08 18:4x-18:5x, binding): *"it is the work done in
real time; forget Needs You; stage means show it to me and wait for my word in
this conversation, never a row in a table."* Measured the same day at 21:2x: the
app daemons were stopped on both Macs and every write verb died --
`snappy-linkedin post` exited 1 with
`not staged: {"error":"stage_door_unreachable"...}` -- while the owner sat at the
keyboard ready to say yes.

Importers, measured 2026-09-08 (`grep -l "snappy-settings/stage.ts" skills/*/api.ts`), **8**:
`snappy-freshbooks`, `snappy-gmail`, `snappy-hands`, `snappy-imessage`,
`snappy-linkedin`, `snappy-skool`, `snappy-slack`, `snappy-telegram`.

## The provider choices -- `providers-choice.ts` (THE ONE READER)

⟨owner, 2026-09-09 10:25, on the bar's Providers tab⟩ *"I choose one for
default, I choose the fallback, I choose the level being used for each one …
it should all be controllable and saveable there — a big asset."*

The bar saves those three choices to **`~/.snappy-skills/providers.json`**:

```json
{ "default": "chatgpt",
  "fallback": ["openrouter", "claude"],
  "effort": { "chatgpt": "xhigh", "claude": "low" },
  "at": "2026-09-09T14:22:31.004Z" }
```

**`providers-choice.ts` is the kernel's ONE reader of that document. Never
write a second parser of it, anywhere.** The file has exactly one writer —
`writeChoices()` in `snappy-runner/src/providers.ts` — and the two repos cannot
share a module, so the contract is held by citation: every field filter mirrors
that file's `readChoices()`, and the four effort words are its `EFFORTS`. This
reader never writes, never repairs, and never performs the runner's
`~/.snappy-runner` → `~/.snappy-skills` rename.

```typescript
import { providerChoices, defaultProvider, fallbackOrder, providerOrder, effortFor }
  from "../snappy-settings/providers-choice.ts";
```

| Function | Answers |
|---|---|
| `providerChoices()` | The whole document, re-read per call, plus `source`: `"document"` / `"absent"` / `"unreadable"` |
| `defaultProvider()` | The provider he reaches for first, or `null` if he never chose |
| `fallbackOrder()` | His fallback list exactly as saved |
| `providerOrder()` | **The one ordering to walk**: default first, then the fallback, each id once |
| `effortFor(id)` | His level for one provider, or `null` — `null` is "no preference", never "medium" |
| `providersChoicePath()` | The path, honouring `SNAPPY_SKILLS_HOME` (read per call, so a test can point it at a scratch dir) |

**The ids are the runner's, minted from `jcode usage --json`:** `chatgpt`,
`openrouter`, `claude`, `openai-api`, `gemini`. They are NOT jcode's `-p`
words and NOT the model aliases; a skill that spends a provider owns the map
from an id to its own runner's name and says which map it used.

**An absent or unparseable document answers the defaults and says so** — it
never throws and never rewrites the file. Unknown effort words are dropped by
name; the known ones beside them survive.

**Who reads it (measured 2026-09-09):** `snappy-jcode` (provider + per-run
effort via `PROVIDER_ROADS`), `snappy-shell` (`auto` cascade order via
`cascadeAliases()`), `snappy-dispatch` (the alias an unasked errand spends via
`defaultAlias()`).

## The credential file

```
~/.claude/skills/snappy-settings/.env.cache    # chmod 600, plain KEY=value
```

Edit it with any editor. Lines starting with `#` are comments. Empty values (`KEY=`) are treated as unset -- callers should handle missing credentials explicitly.

## Credential catalog

Canonical status (updated 2026-04-08 after Bitwarden removal + bulk paste).

### Xano
| Env var | Used by |
|---|---|
| `XANO` | Base URL `https://xnwv-v1z6-dvnr.n7c.xano.io` |
| `XANO_METADATA_TOKEN` | snappy-knowledge, snappy-pipeline, snappy-infra metadata ops (single Xano auth token) |

### AI / LLM providers
| Env var | Used by |
|---|---|
| `OPENAI_API_KEY` | snappy-ai-models, snappy-image (DALL-E), snappy-video (Whisper) |
| `OPENAI_ASSISTANT_ID` | snappy-ai-models (legacy assistants) |
| `ANTHROPIC_API_KEY` | **intentionally empty** -- Claude Code subagents use parent session. Only set if a skill needs direct SDK calls. |
| `GEMINI_API_KEY` | snappy-gemini, snappy-image (Nano Banana / Imagen) |
| `OPENROUTER_API_KEY` | snappy-openrouter -- direct-API fallback only |
| `REPLICATE_API_TOKEN` | snappy-image, snappy-video |

### Media generation (image / video / audio)
| Env var | Used by |
|---|---|
| `COMFYICU_API_KEY` | snappy-image (ComfyICU workflows) |
| `FAL_API_KEY` | snappy-image, snappy-video (fal.ai) |
| `SEGMIND_API_KEY` | snappy-image (Segmind) |
| `ELEVENLABS_API_KEY` | snappy-video (voiceover) |
| `DEEPGRAM_API_KEY` | snappy-video, snappy-transcripts |

### Messaging
| Env var | Used by |
|---|---|
| `SLACK_USER_TOKEN` | snappy-slack (primary, `xoxp-`) |
| `SLACK_BOT_TOKEN` | snappy-slack (fallback, `xoxb-`) |
| `TELEGRAM_BOT_TOKEN` | snappy-telegram, snappy-clients |
| `TELEGRAM_ROBERT_CHAT_ID` | snappy-telegram (Robert's DM chat id) |
| `WHATSAPP_TOKEN` | snappy-whatsapp (Meta Cloud API access token) -- **empty** |
| `WHATSAPP_PHONE_ID` | snappy-whatsapp (phone number ID) -- **empty** |
| `ROBERT_PHONE` | snappy-whatsapp (Robert's E.164 phone) -- **empty** |

### Docs / content distribution
| Env var | Used by |
|---|---|
| `NOTION_TOKEN` | snappy-docs, snappy-notion |
| `TYPEFULLY_API_KEY` | snappy-linkedin (scheduled posting) |
| `LATE_API_KEY` | Late.so social scheduling |
| `LOOPS_API_KEY` | snappy-email (Loops.so transactional) |

### LinkedIn (OAuth)
| Env var | Used by |
|---|---|
| `LINKEDIN_CLIENT_ID` | snappy-linkedin OAuth flow |
| `LINKEDIN_CLIENT_SECRET` | snappy-linkedin OAuth flow |
| `LINKEDIN_ACCESS_TOKEN` | snappy-linkedin posting -- **empty, regenerate via OAuth flow** |
| `LINKEDIN_AUTH` | snappy-linkedin legacy cookie state -- **empty** |

### YouTube (OAuth)
| Env var | Used by |
|---|---|
| `YOUTUBE_CLIENT_ID` | snappy-youtube OAuth flow |
| `YOUTUBE_CLIENT_SECRET` | snappy-youtube OAuth flow |
| `YOUTUBE_ACCESS_TOKEN` | snappy-youtube write ops -- **empty, regenerate via OAuth flow** |

### Billing
| Env var | Used by |
|---|---|
| `STRIPE_SECRET_KEY` | snappy-analytics (revenue), live key |

### FreshBooks -- **intentionally empty until trust rebuilt**
| Env var | Used by |
|---|---|
| `FRESHBOOKS_CLIENT_ID` | snappy-freshbooks -- **DO NOT FILL** until Robert authorizes |
| `FRESHBOOKS_CLIENT_SECRET` | snappy-freshbooks -- **DO NOT FILL** |
| `FRESHBOOKS_REFRESH_TOKEN` | snappy-freshbooks -- **DO NOT FILL** |
| `FRESHBOOKS_ACCOUNT_ID` | snappy-freshbooks -- **DO NOT FILL** |

### Zoom (OAuth)
| Env var | Used by |
|---|---|
| `ZOOM_ACCOUNT_ID` | snappy-scheduling (meeting creation) |
| `ZOOM_CLIENT_ID` | snappy-scheduling OAuth |
| `ZOOM_CLIENT_SECRET` | snappy-scheduling OAuth |
| `ZOOM_SECRET_TOKEN` | snappy-scheduling webhook verification |

### Infrastructure
| Env var | Used by |
|---|---|
| `GITHUB_TOKEN` | snappy-github, snappy-publish (primary PAT) |
| `GITHUB_PAT` | snappy-github (secondary PAT, for dev-only repos) |
| `VERCEL_TOKEN` | snappy-publish, snappy-website -- **currently placeholder `password`, needs real token** |
| `CLOUDFLARE_API_TOKEN` | snappy-infra, snappy-gateway |
| `CLOUDFLARE_ACCOUNT_ID` | snappy-infra, snappy-gateway |
| `DO_SPACES_KEY` | DigitalOcean Spaces access |
| `DO_SPACES_SECRET` | DigitalOcean Spaces secret |
| `DO_SPACES_BUCKET` | snappy-image CDN bucket (`robert-storage`) |
| `DO_SPACES_REGION` | `tor1` |
| `DO_SPACES_ENDPOINT` | `https://tor1.digitaloceanspaces.com` |

### Browser automation
| Env var | Used by |
|---|---|
| `BROWSERBASE_API_KEY` | snappy-browse cloud browsers |
| `BROWSERBASE_PROJECT_ID` | snappy-browse cloud browsers |

### Google Service Account (calendar, drive, gmail, sheets, docs)
| Env var | Used by |
|---|---|
| `GOOGLE_SERVICE_ACCOUNT_EMAIL` | snappy-calendar, snappy-email, snappy-docs |
| `GOOGLE_SERVICE_ACCOUNT_KEY` | PEM with literal `\n` -- decode via `.replace(/\\n/g, '\n')` before passing to googleapis |
| `GOOGLE_CLIENT_ID` | snappy-email, snappy-inbox-sweep (OAuth client for personal Gmail account) |
| `GOOGLE_CLIENT_SECRET` | snappy-email, snappy-inbox-sweep (OAuth client secret for personal Gmail account) |

### Agent infrastructure (internal)
| Env var | Used by |
|---|---|
| `OPENCLAW_GATEWAY_URL` | snappy-infra (Mac Mini tailscale gateway) |
| `OPENCLAW_GATEWAY_TOKEN` | snappy-infra auth |
| `AGENT_EVENT_TOKEN` | snappy-ops event bus auth |
| `FALKORDB_API_KEY` | snappy-knowledge graph DB |
| `SNAPPY_MASTER_KEY` | snappy-email, snappy-inbox-sweep (master symmetric key for local credential encryption) |

All skills call their service APIs directly. Xano is only used where it IS the database (snappy-knowledge, snappy-pipeline).

## Rules

- **NEVER hardcode a token** in a script, SKILL.md, AGENTS.md, or anywhere else. Always use `env("KEY")` or `$KEY`.
- **NEVER paste a token into a prompt** or echo it to logs. Use length checks: `${OPENAI_API_KEY:+set}`.
- **NEVER commit `.env.cache`.**
- **NEVER pass a key as a CLI arg** -- it leaks via `ps aux`. Env vars only.
- **NEVER reintroduce Bitwarden or another sync layer.** That is the exact loop that wiped credentials on 2026-04-08. The file is the source of truth. Anything that rewrites `.env.cache` from elsewhere will destroy working data.

## Adding a new credential

1. Add `NEW_KEY=value` to `.env.cache`
2. Read it: `env("NEW_KEY")` from a skill's `api.ts` (or `$NEW_KEY` after sourcing `load-env.sh`)
3. Add a row to the catalog table above

Three steps. Nothing to regenerate.

## Failure modes

- `.env.cache` doesn't exist → STOP, tell the user to create it. Don't fabricate credentials.
- A required key is empty → STOP, tell the user which key is missing and which skill needs it. Don't silently fall back.
- Unknown key name → STOP, run `check-creds.sh` and list what's actually in the file.

## Uses

Every `snappy-*` skill that hits an external API reads from here via `env()`. The credential catalog above is the full list.

---

If this loader doesn't cover your case, read `SKILL.md`. If that still doesn't cover it:
```bash
echo "[$(date -u +%FT%TZ)] snappy-settings: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
```

<!-- SKILL-INDEX-START -->
[snappy-settings Index]|root: ~/.claude/skills/snappy-settings|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,skill-spec.md}|docs:{consolidation-candidates.md,consolidation-decisions-needed-2026-04-11.md,snappy-system-review.md,snappy-system.md}
<!-- SKILL-INDEX-END -->

## Used by

- `snappy-ffmpeg`
- `snappy-image`
- `snappy-inbox-sweep`
- `snappy-openrouter`
- `snappy-os-operator`
- `snappy-skill`
- `snappy-sync`

<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs

Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.

| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `check` | `key` | `read` | `npx tsx ~/.claude/skills/snappy-settings/api.ts check <key>` |
| `list` | — | `read` | `npx tsx ~/.claude/skills/snappy-settings/api.ts list` |

## Show the result

When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->

Keyboard Shortcuts

Search in document⌘K
Focus search/
Previous file tab
Next file tab
Close overlayEsc
Show shortcuts?