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) <noreply@anthropic.com>
This commit is contained in:
rshtirmer
2026-03-10 17:38:44 -04:00
parent a6ed03b98e
commit 66e591a2a4
9 changed files with 242 additions and 18 deletions
+4
View File
@@ -22,5 +22,9 @@ test-*.png
# Gallery build artifacts
gallery/thumbnails/
# API keys (managed by intercept hook)
.env
.env.local
# Deployment artifacts
.herenow/
+162
View File
@@ -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
});
+13
View File
@@ -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
}
]
}
]
}
}
+9 -2
View File
@@ -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
+17 -5
View File
@@ -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 "<character name> 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=<token> node scripts/find-3d-asset.mjs ...`
Then use it via: `source .env && node scripts/find-3d-asset.mjs ...`
## Search & Download Script
+7 -1
View File
@@ -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 "<API_KEY>" > ~/.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
+4 -3
View File
@@ -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
+12 -3
View File
@@ -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=<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).
+14 -4
View File
@@ -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=<your-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