Refuses the Bash and Edit calls that would spend render credit, post, or edit .env by accident. The click is the approval; this keeps a session from clicking…

An AI production studio. Bring an idea and your own photos; get stills and video made on current image and video models, held to those photos, and pay in credits only for what you approve. Live at phantra.ai. (Named Zero Page until 2026-10-13.)
https://api.phantra.ai/mcp and https://api.phantra.ai/mcp/claude. See phantra.ai/connector.| Part | Where |
|---|---|
| Site and studio | Next.js in web/, on Vercel |
| API | FastAPI in app/ over the library in src/, on Fly |
| Data and sign-in | Postgres and Auth on Supabase |
| Media | Cloudflare R2, served at media.phantra.ai |
| Video and image models | fal.ai (Kling, Veo, Seedance, LTX, Wan, FLUX and others) and Gemini |
| Billing | Stripe plans that grant credits; a ledger hold before every provider call |
venv/bin/pip install -r requirements.txt
venv/bin/pip install -e .
venv/bin/uvicorn app.main:app --reload # the API on 127.0.0.1:8000
cd web && npm install && npm run dev # the studio on localhost:3000
.env.example and web/.env.example list the settings. START_SERVER.md covers the local server and the MCP mount; CLAUDE.md is the working guide to the code, commands and conventions; docs/ holds the task files, the billing design and the runbook.
venv/bin/python -m pytest tests/ -q
venv/bin/ruff check .
cd web && node --test tests/*.test.mjs && npm run typecheck && npm run build
The test suite blocks all network access, so a test cannot make a billed call.
The README this replaced is kept at docs/retired/README-before-phantra.md.
hooks/register.ts 70 lines1import type { Register } from 'claude-code'
2
3// Every switch in this repo that turns a dry run into money or a post. CLAUDE.md:
4// "the click is the approval" -- a person presses Approve in the Queue, or exports
5// a *_SPEND_OK on purpose. A session arming one of these in a Bash command is the
6// exact "approval that's always on" the gates were written to prevent.
7const ARMS: readonly [RegExp, string][] = [
8 [/\b[A-Z0-9_]*_SPEND_OK=1\b/, 'arms a *_SPEND_OK render approval'],
9 [/\bPHANTRA_RENDER=1\b/, 'turns the graph render from a dry stub into real spend'],
10 [/\bPHANTRA_AUTOPILOT=1\b/, 'arms the L4 autopilot'],
11 [/\bPHANTRA_POST_OK=1\b/, 'arms posting'],
12 [/\bsrc\.autopilot\s+run\b[^\n]*--approve/, 'runs the autopilot live'],
13 [/\bsrc\.scheduling\s+run\b[^\n]*--live/, 'publishes the scheduled queue live'],
14 [/\b(ops\.stripe_setup|ops\/stripe_setup\.py)\b[^\n]*--live/, 'switches Stripe to live mode'],
15]
16
17// 2026-09-28 "Stop local tests from reaching the live database": the pattern that
18// did it was `set -a && source .env && set +a` followed by pytest in one line.
19const LIVE_DB_TESTS = /source\s+\.env\b[\s\S]*\bpytest\b|\bpytest\b[\s\S]*source\s+\.env\b/
20
21// .env, .env.local, .env.bak.* -- never .env.example, which is checked in.
22const ENV_FILE = /(^|\/)\.env(\.(?!example$)[^/]*)?$/
23
24// The override, for the one case the person DID ask for the spend this session:
25// `# spend-ok` at the end of the command. The deny text says when it is allowed.
26const OVERRIDE = /#\s*spend-ok\b/
27
28export const register: Register = on => {
29 on('tool.call', { tool: 'Bash' }, ($, e, next) => {
30 const cmd = e.command
31 if (OVERRIDE.test(cmd)) return next(e)
32
33 const hit = ARMS.find(([re]) => re.test(cmd))
34 if (hit) {
35 $.ui.toast(`spend-guard refused a command that ${hit[1]}`)
36 $.ui.status(`spend-guard: refused (${hit[1]})`)
37 return {
38 deny:
39 `phantra-spend-guard: this command ${hit[1]}. The click in the Queue is the approval; ` +
40 `a session must not arm it. If Mike asked for exactly this spend in his own words ` +
41 `this session, re-run with \`# spend-ok\` at the end of the command and say so.`,
42 }
43 }
44
45 if (LIVE_DB_TESTS.test(cmd)) {
46 $.ui.toast('spend-guard: pytest with .env sourced -- refused (live DATABASE_URL)')
47 return {
48 deny:
49 'phantra-spend-guard: `source .env` and `pytest` in one command points the test suite at the ' +
50 'LIVE DATABASE_URL (2026-09-28, docs/RUNBOOK.md). Run the tests without sourcing .env.',
51 }
52 }
53
54 return next(e)
55 })
56
57 const protectEnv = (file: string) =>
58 ENV_FILE.test(file)
59 ? {
60 deny:
61 `phantra-spend-guard: ${file} holds the live keys and tokens and is written only by the ` +
62 `scripts that back it up first (ops.ig_tokens, ops.stripe_setup). Edit .env.example, ` +
63 `or tell Mike the exact line to change.`,
64 }
65 : null
66
67 on('tool.call', { tool: 'Edit' }, ($, e, next) => protectEnv(e.file_path) ?? next(e))
68 on('tool.call', { tool: 'Write' }, ($, e, next) => protectEnv(e.file_path) ?? next(e))
69}
70