mirror of
https://github.com/PlayableIntelligence/game-creator.git
synced 2026-09-19 07:34:10 +08:00
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:
@@ -22,5 +22,9 @@ test-*.png
|
||||
# Gallery build artifacts
|
||||
gallery/thumbnails/
|
||||
|
||||
# API keys (managed by intercept hook)
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Deployment artifacts
|
||||
.herenow/
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user