From 66e591a2a4455a9989402e000eed1a1df4e4dd71 Mon Sep 17 00:00:00 2001 From: rshtirmer Date: Tue, 10 Mar 2026 17:38:44 -0400 Subject: [PATCH] feat(security): intercept API keys pasted in chat via UserPromptSubmit hook API keys pasted in chat previously ended up in Claude's context window. This adds a UserPromptSubmit hook that detects key patterns, saves them to .env, and blocks the message so keys never reach the model. - Add scripts/intercept-api-key.mjs (zero deps, explicit + bare key detection) - Add hooks.UserPromptSubmit config to settings.json - Add .env to .gitignore - Update 6 skill prompts to use KEY=value paste format with .env check-first Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 4 + scripts/intercept-api-key.mjs | 162 +++++++++++++++++++++++++++++++++ settings.json | 13 +++ skills/add-3d-assets/SKILL.md | 11 ++- skills/game-3d-assets/SKILL.md | 22 ++++- skills/game-deploy/SKILL.md | 8 +- skills/make-game/SKILL.md | 7 +- skills/meshyai/SKILL.md | 15 ++- skills/worldlabs/SKILL.md | 18 +++- 9 files changed, 242 insertions(+), 18 deletions(-) create mode 100644 scripts/intercept-api-key.mjs diff --git a/.gitignore b/.gitignore index 8b56578..23344dc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,9 @@ test-*.png # Gallery build artifacts gallery/thumbnails/ +# API keys (managed by intercept hook) +.env +.env.local + # Deployment artifacts .herenow/ diff --git a/scripts/intercept-api-key.mjs b/scripts/intercept-api-key.mjs new file mode 100644 index 0000000..1229628 --- /dev/null +++ b/scripts/intercept-api-key.mjs @@ -0,0 +1,162 @@ +#!/usr/bin/env node +/** + * UserPromptSubmit hook: intercepts API keys pasted in chat, + * saves them to .env, and blocks the message so keys never + * reach the context window. + * + * Exit codes: + * 0 — pass through (no key detected) + * 2 — block (key saved to .env) + */ + +import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +// ── Key patterns ────────────────────────────────────────────── +const EXPLICIT_KEYS = [ + 'MESHY_API_KEY', + 'WORLDLABS_API_KEY', + 'WLT_API_KEY', + 'SKETCHFAB_TOKEN', + 'POLY_PIZZA_API_KEY', + 'HERENOW_API_KEY', +]; + +const EXPLICIT_RE = new RegExp( + `^(${EXPLICIT_KEYS.join('|')})\\s*[=:]\\s*(\\S+)`, + 'gm' +); + +// Bare key prefix → env var name +const PREFIX_MAP = { + msy_: 'MESHY_API_KEY', + wlt_: 'WLT_API_KEY', + sk_: 'SKETCHFAB_TOKEN', +}; + +// ── Helpers ─────────────────────────────────────────────────── + +function findProjectRoot(cwd) { + let dir = resolve(cwd); + for (let i = 0; i < 5; i++) { + if (existsSync(join(dir, 'package.json'))) return dir; + const parent = resolve(dir, '..'); + if (parent === dir) break; + dir = parent; + } + return resolve(cwd); +} + +function readEnv(envPath) { + if (!existsSync(envPath)) return ''; + return readFileSync(envPath, 'utf8'); +} + +function upsertEnv(envPath, key, value) { + let content = readEnv(envPath); + const lineRe = new RegExp(`^${key}=.*$`, 'm'); + + if (lineRe.test(content)) { + content = content.replace(lineRe, `${key}=${value}`); + } else { + if (!content && !existsSync(envPath)) { + content = '# API keys (managed by game-creator hook — do not commit)\n'; + } + if (content.length > 0 && !content.endsWith('\n')) content += '\n'; + content += `${key}=${value}\n`; + } + + writeFileSync(envPath, content, 'utf8'); + try { chmodSync(envPath, 0o600); } catch { /* best effort */ } +} + +// ── Main ────────────────────────────────────────────────────── + +async function main() { + let input = ''; + for await (const chunk of process.stdin) input += chunk; + + const { prompt, cwd } = JSON.parse(input); + if (!prompt || !cwd) process.exit(0); + + const saved = []; + const root = findProjectRoot(cwd); + const envPath = join(root, '.env'); + + // 1. Explicit format: KEY=value or KEY: value + let match; + EXPLICIT_RE.lastIndex = 0; + while ((match = EXPLICIT_RE.exec(prompt)) !== null) { + const [, key, value] = match; + try { + upsertEnv(envPath, key, value); + saved.push(key); + process.stderr.write(`\u2713 Saved ${key} to ${envPath} (redacted from conversation)\n`); + } catch (err) { + process.stderr.write(`Warning: could not save ${key} to .env: ${err.message}\n`); + } + } + + // 2. Bare key heuristic (only if no explicit matches) + if (saved.length === 0) { + const trimmed = prompt.trim(); + + // Must be single-line, no spaces, no URLs, 20+ chars + if ( + !trimmed.includes('\n') && + !trimmed.includes(' ') && + !trimmed.includes('://') && + trimmed.length >= 20 && + /^[a-zA-Z0-9_\-]+$/.test(trimmed) + ) { + // Try to infer key name from prefix + let keyName = null; + const lower = trimmed.toLowerCase(); + for (const [prefix, envVar] of Object.entries(PREFIX_MAP)) { + if (lower.startsWith(prefix)) { + keyName = envVar; + break; + } + } + + if (keyName) { + try { + upsertEnv(envPath, keyName, trimmed); + saved.push(keyName); + process.stderr.write(`\u2713 Saved ${keyName} to ${envPath} (redacted from conversation)\n`); + } catch (err) { + process.stderr.write(`Warning: could not save ${keyName} to .env: ${err.message}\n`); + } + } + // Unknown prefix with 32+ chars — could be a key but we can't be sure which one. + // Pass through rather than risk misidentifying. + } + } + + if (saved.length === 0) { + process.exit(0); // No key detected — pass through + } + + // Block the message and inject context + const keyList = saved.join(', '); + const result = { + decision: 'block', + reason: `API key${saved.length > 1 ? 's' : ''} saved to .env`, + hookSpecificOutput: { + hookEventName: 'UserPromptSubmit', + additionalContext: [ + `The user's ${keyList} ha${saved.length > 1 ? 've' : 's'} been securely saved to ${envPath}.`, + `Use ${saved.length > 1 ? 'them' : 'it'} with: source ${envPath} && node scripts/meshy-generate.mjs ...`, + 'Do NOT ask the user to provide this key again.', + ].join(' '), + }, + }; + + process.stdout.write(JSON.stringify(result)); + process.exit(2); +} + +main().catch((err) => { + process.stderr.write(`intercept-api-key error: ${err.message}\n`); + process.exit(0); // Graceful degradation — pass through +}); diff --git a/settings.json b/settings.json index ea67094..dc72b60 100644 --- a/settings.json +++ b/settings.json @@ -2,5 +2,18 @@ "agent": "game-creator", "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" + }, + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "node scripts/intercept-api-key.mjs", + "timeout": 5 + } + ] + } + ] } } diff --git a/skills/add-3d-assets/SKILL.md b/skills/add-3d-assets/SKILL.md index 17ede58..fa6b8b6 100644 --- a/skills/add-3d-assets/SKILL.md +++ b/skills/add-3d-assets/SKILL.md @@ -27,14 +27,21 @@ First, load the game-3d-assets skill and the meshyai skill for the full model pi ### Step 1: Get Meshy API Key -Check if `MESHY_API_KEY` is set. If not, ask the user: +Check if `MESHY_API_KEY` is set. First check `.env`: +`test -f .env && grep -q MESHY_API_KEY .env && echo "found"` +If found, load it with `source .env` and skip the prompt. + +If not set, ask the user: > I'll generate custom 3D models with Meshy AI for the best results. You can get a free API key in 30 seconds: > 1. Sign up at https://app.meshy.ai > 2. Go to Settings → API Keys > 3. Create a new API key > -> What is your Meshy API key? (Or type "skip" to use free model libraries instead) +> Paste your key below like: `MESHY_API_KEY=your-key-here` +> (It will be saved to .env and redacted from this conversation automatically.) +> +> Or type "skip" to use free model libraries instead. ### Step 2: Audit diff --git a/skills/game-3d-assets/SKILL.md b/skills/game-3d-assets/SKILL.md index 58b81af..b80e29c 100644 --- a/skills/game-3d-assets/SKILL.md +++ b/skills/game-3d-assets/SKILL.md @@ -30,14 +30,21 @@ Primitive cubes and spheres are fast to scaffold, but players can't tell a house ### Meshy API Key -Meshy AI is the **preferred source for all 3D assets**. If `MESHY_API_KEY` is not set, **ask the user before falling back to other tiers**: +Meshy AI is the **preferred source for all 3D assets**. Before prompting the user, check if the key already exists: +`test -f .env && grep -q MESHY_API_KEY .env && echo "found"` +If found, load it with `source .env` and skip the prompt. + +If `MESHY_API_KEY` is not set, **ask the user before falling back to other tiers**: > I'd like to generate custom 3D models with Meshy AI for the best results. You can get a free API key: > 1. Sign up at https://app.meshy.ai > 2. Go to Settings → API Keys > 3. Create a new API key > -> What is your Meshy API key? (Or type "skip" to use free model libraries instead) +> Paste your key below like: `MESHY_API_KEY=your-key-here` +> (It will be saved to .env and redacted from this conversation automatically.) +> +> Or type "skip" to use free model libraries instead. If the user provides a key, use it for all `meshy-generate.mjs` calls. If they skip, fall through to Tier 2+. @@ -139,16 +146,21 @@ node scripts/find-3d-asset.mjs --query " animated character" --m ## Sketchfab Token (Tier 3 fallback) -Only needed if falling back to Sketchfab. Search is free but **download requires `SKETCHFAB_TOKEN`**. If needed and not set, ask the user: +Only needed if falling back to Sketchfab. Search is free but **download requires `SKETCHFAB_TOKEN`**. Before prompting, check if the key already exists: +`test -f .env && grep -q SKETCHFAB_TOKEN .env && echo "found"` +If found, load it with `source .env` and skip the prompt. + +If needed and not set, ask the user: > I need a Sketchfab API token to download this model. You can get one for free: > 1. Sign in at https://sketchfab.com > 2. Go to https://sketchfab.com/settings/password → "API Token" > 3. Copy the token > -> What is your Sketchfab API token? +> Paste your token below like: `SKETCHFAB_TOKEN=your-token-here` +> (It will be saved to .env and redacted from this conversation automatically.) -Then pass it as: `SKETCHFAB_TOKEN= node scripts/find-3d-asset.mjs ...` +Then use it via: `source .env && node scripts/find-3d-asset.mjs ...` ## Search & Download Script diff --git a/skills/game-deploy/SKILL.md b/skills/game-deploy/SKILL.md index c15c6d8..cae5d37 100644 --- a/skills/game-deploy/SKILL.md +++ b/skills/game-deploy/SKILL.md @@ -62,10 +62,16 @@ Without an API key, publishes are **anonymous and expire in 24 hours**. The publ To set up an API key for permanent hosting (skip the 24h window entirely): +First check if the key already exists: +`test -f .env && grep -q HERENOW_API_KEY .env && echo "found"` +If found, load it with `source .env` and skip the prompt. + +Otherwise: 1. Ask the user for their email 2. Send a magic link: `curl -sS https://here.now/api/auth/login -H "content-type: application/json" -d '{"email": "user@example.com"}'` 3. User clicks the link, copies their API key from the dashboard -4. Save the key: `mkdir -p ~/.herenow && echo "" > ~/.herenow/credentials && chmod 600 ~/.herenow/credentials` +4. User pastes the key like: `HERENOW_API_KEY=their-key-here` (saved to .env and redacted automatically) +5. Also save to credentials file: `mkdir -p ~/.herenow && grep HERENOW_API_KEY .env | cut -d= -f2 > ~/.herenow/credentials && chmod 600 ~/.herenow/credentials` ### Updating a deploy diff --git a/skills/make-game/SKILL.md b/skills/make-game/SKILL.md index 16ddf13..51f6070 100644 --- a/skills/make-game/SKILL.md +++ b/skills/make-game/SKILL.md @@ -86,10 +86,11 @@ Parse `$ARGUMENTS` to determine the game concept. Arguments can take two forms: #### 3D API Keys -For 3D games, check for these API keys in the environment: +For 3D games, check for these API keys — first in `.env` (`test -f .env && grep -q KEY_NAME .env`), then in the environment: - **`MESHY_API_KEY`** — for generating custom 3D character/prop models with Meshy AI (see [tweet-pipeline.md](tweet-pipeline.md) for the prompt flow) -- **`WLT_API_KEY`** — for generating photorealistic 3D environments with World Labs Gaussian Splats. If not set, ask the user alongside `MESHY_API_KEY`: - > I can also generate a **photorealistic 3D environment** with World Labs. Get a free API key at https://worldlabs.ai — or type "skip" to use basic geometry. +- **`WLT_API_KEY`** / **`WORLDLABS_API_KEY`** — for generating photorealistic 3D environments with World Labs Gaussian Splats. If not set, ask the user alongside `MESHY_API_KEY`: + > I can also generate a **photorealistic 3D environment** with World Labs. Paste your key like: `WORLDLABS_API_KEY=your-key-here` — or type "skip" to use basic geometry. + > (Keys are saved to .env and redacted from this conversation automatically.) #### Form B: Tweet URL as game concept diff --git a/skills/meshyai/SKILL.md b/skills/meshyai/SKILL.md index c683f84..5e8df12 100644 --- a/skills/meshyai/SKILL.md +++ b/skills/meshyai/SKILL.md @@ -49,16 +49,25 @@ If `MESHY_API_KEY` is not available and the user declines to set one up, fall ba ## Authentication -All Meshy API calls require `MESHY_API_KEY`. **Always check for this key before starting any 3D asset work.** If the key is not set in the environment, **ask the user immediately**: +All Meshy API calls require `MESHY_API_KEY`. **Always check for this key before starting any 3D asset work.** + +Before prompting the user, check if the key already exists: +`test -f .env && grep -q MESHY_API_KEY .env && echo "found"` +If found, load it with `source .env` and skip the prompt. + +If the key is not set in the environment or `.env`, **ask the user immediately**: > I'll generate custom 3D models with Meshy AI for the best results. You can get a free API key in 30 seconds: > 1. Sign up at https://app.meshy.ai > 2. Go to Settings → API Keys > 3. Create a new API key > -> What is your Meshy API key? (Or type "skip" to use free model libraries instead) +> Paste your key below like: `MESHY_API_KEY=your-key-here` +> (It will be saved to .env and redacted from this conversation automatically.) +> +> Or type "skip" to use free model libraries instead. -If the user provides a key, use it via: `MESHY_API_KEY= node scripts/meshy-generate.mjs ...` +If the user provides a key, use it via: `source .env && node scripts/meshy-generate.mjs ...` If the user skips, proceed with fallback sources (character library → Sketchfab → Poly Haven). diff --git a/skills/worldlabs/SKILL.md b/skills/worldlabs/SKILL.md index f1ab063..3215dbc 100644 --- a/skills/worldlabs/SKILL.md +++ b/skills/worldlabs/SKILL.md @@ -48,11 +48,21 @@ When the game-creator pipeline runs, **ask the user for a reference image first* ## Environment Variable -```bash -export WORLDLABS_API_KEY= -``` +Before prompting the user, check if the key already exists: +`test -f .env && grep -q WORLDLABS_API_KEY .env && echo "found"` +If found, load it with `source .env` and skip the prompt. -Get an API key at: https://platform.worldlabs.ai/api-keys +If not set, ask the user: + +> I'll generate a photorealistic 3D environment with World Labs. You can get a free API key: +> 1. Sign up at https://platform.worldlabs.ai +> 2. Go to API Keys +> 3. Create a new key +> +> Paste your key below like: `WORLDLABS_API_KEY=your-key-here` +> (It will be saved to .env and redacted from this conversation automatically.) +> +> Or type "skip" to use basic geometry instead. ## CLI Script Usage