phantom CLAUDE-PHANTOM

your app crashed.phantom is on it.

Wrap any command. If it dies, a headless Claude Code session diagnoses the bug, writes a failing test, patches it on a separate branch, verifies the fix independently, and leaves a post-mortem. Your branch is never touched.

$ npm install -g claude-phantom
$ phantom npm run dev

or install from GitHubnpm i -g github:waazy-w/claude-phantom

v0.1.1 dependencies 0 node >=18 MIT 193 tests
zsh — phantom npm start
recorded, not staged

One real recovery, start to finish.

A guest order with no customer takes down a Node service. Phantom catches the exit, opens a fix branch, patches it, runs the tests itself, and writes the post-mortem — while you watch.

Terminal recording: a Node service throws a TypeError on a guest order, phantom detects the crash, opens a fix branch, patches the code, runs the test suite, and prints a FIXED banner with the report path. Terminal recording: a Node service throws a TypeError on a guest order, phantom detects the crash, opens a fix branch, patches the code, runs the test suite, and prints a FIXED banner with the report path.

examples/crash-demo · one iteration · 1m 48s wall clock

how it works

Invisible until the exit code isn't zero.

stdout, stderr and stdin stream through byte-for-byte and your exit code is preserved. Phantom keeps the last 256 KiB in a ring buffer and does nothing else — until your process exits non-zero or dies from a signal that wasn't your own Ctrl+C.

01

passthrough

Your command runs exactly as before. A 256 KiB ring buffer holds the tail of the output; overhead is a child-process spawn and nothing else.

02

capture

On a crash, phantom extracts the stack trace, the output tail, git state and your package.json. Terminal escapes are stripped so paths survive intact and secrets can't hide mid-token.

.phantom/crashes/*.json
03

safety checks

Clean tree? Inside a git repo? Is claude on the PATH? Then, and only then, git checkout -b phantom/fix-<slug>-<ts> from HEAD — before a single edit.

04

diagnose

A headless claude -p session starts with a minimal tool allowlist, never-touch globs denied, no network and no push path.

05

failing test first

The session reproduces the crash as a test before it changes any source, so the fix is pinned by something that failed a moment ago.

06

independent verification

Phantom runs your test command itself, outside the session. If it fails, the session is resumed with the real output — bounded by maxIterations and maxMinutes.

07

audit, commit, post-mortem

The branch is audited against the starting commit. Any never-touch hit discards the work. Then a commit on the fix branch, a post-mortem, and you're back on your branch.

.phantom/reports/*.md

Nothing here trusts the agent's own word. Phantom runs your tests itself and audits the branch after the session ends.

safety rails

An agent with commit access is a bad idea. This one has none.

Phantom is built on the assumption that the session will eventually do something wrong. Every rail below is enforced mechanically, not requested in a prompt.

never your branch

A fix branch is cut from HEAD before any edit, and your branch is checked back out when phantom finishes — success or failure. The fix exists only as a branch to diff, merge, or delete.

no pushes, no PRs

git push is a denied tool, there is no network tool, and phantom has no push code path. Not configurable.

minimal tools

An explicit allowlist: read, edit, grep, glob, your test command, node, and read-only git. Everything else is denied without prompting.

guard hook

A zero-dependency PreToolUse hook that fails closed, inspecting every call for never-touch paths, destructive shell, installs, migrations and state-changing git.

never-touch, three times over

.env, keys, PEM files and secrets are permission deny rules, checked again by the guard hook, then audited afterwards. Any hit hard-reverts the branch.

isolated session

--setting-sources project,local: your user hooks, permission allows, plugins and their MCP servers are never loaded into the recovery session.

ctrl+c is a kill switch

Kills the process tree, resets and cleans the fix branch, checks out your branch, pops the snapshot stash, exits 130.

hard caps

maxIterations (3) bounds invocations; maxMinutes (15) is a wall-clock timer that kills the child.

off switch

PHANTOM_DISABLED=1 turns phantom into a pure passthrough. No flag to remove, no wrapper to unpick.

not a sandbox

The session may run node — it has to, to run your tests — and a node -e one-liner can in principle read any file your user can. The guard is lexical. Branch isolation, the post-session audit and the no-push rule are the real backstops. Need hard isolation? Run phantom in a container.

redaction

The output tail is scrubbed before the session sees it: KEY=value with secret-looking names, Authorization headers, sk-/ghp_/AKIA/xox tokens, JWTs, URL credentials and PEM blocks. It is pattern-based — a safety net, not a guarantee.

what you get back

A branch, a banner, and a written account.

Treat the branch like a PR from a fast contributor who has never seen your codebase: read the report and the diff, run the tests yourself, then merge or delete.

on your original branch
phantom: ✅ FIXED on phantom/fix-typeerror-…-k3f9a2
  report   .phantom/reports/20260820-184107-…md
  review   git diff main..phantom/fix-…-k3f9a2
  merge    git merge phantom/fix-…-k3f9a2
  discard  git branch -D phantom/fix-…-k3f9a2
  session  192389b5-e0e9-4c66-a16c-a1a9d4f1cd4b

Exit code is always your command's. A fixed crash is still exit 1, so phantom is safe in scripts and && chains.

.phantom/reports/*.md
# Post-mortem: TypeError: Cannot read
# properties of undefined (reading 'email')
Status: ✅ FIXED   Iterations: 1/3   1m 48s

## Root cause
formatOrderLine in src/report.js:9 dereferences
order.customer.email unconditionally; a guest
checkout in data/orders.json has no customer.

## Fix
-  const email = order.customer.email;
+  const email = order.customer?.email ?? '(guest)';

## Verification (independent)
Reproduce (pre-fix)  npm test   ❌ 1 failed
Verify (post-fix)    npm test   ✅ 5 passed
Original command     npm start  ✅ exit 0
Never-touch audit: clean

The verification table and metadata are written by phantom, not the session.

usage

Flags before the command. Everything after it is yours.

phantom [flags] [--] <command> [args...] — everything after the command passes through verbatim, so phantom npm run dev --verbose gives --verbose to npm.

FlagEffect
--dry-runDiagnose and propose a diff; no branch, no edits. The CI-safe mode.
--allow-dirtyProceed with uncommitted changes after taking a stash snapshot.
--test <cmd>Verification command, overriding config and package.json.
--max-iterations <n>Cap on Claude invocations. Default 3, max 10.
--max-minutes <n>Wall-clock cap for the recovery. Default 15, max 120.
--model <m>Passed through as claude --model <m>.
--no-commitLeave the fix uncommitted on the phantom branch and print the way back.
--notifyDesktop notification on crash and when recovery ends.
--verboseStream the session's progress lines.
.phantomrc — every key at its default
{
  "testCommand":     "npm test",
  "maxIterations":   3,
  "maxMinutes":      15,
  "neverTouch":      [".env", ".env.*", "**/*.pem",
                      "**/*.key", "**/secrets/**",
                      "**/*.secret*"],
  "webhook":         null,
  "notify":          false,
  "model":           null,
  "autoCommit":      true,
  "reportDir":       ".phantom/reports",
  "ringBufferBytes": 262144,
  "claudeBin":       "claude"
}
exit codes
your codeAlways. A fixed crash is still exit 1.
128 + sigSignal deaths, like a shell. SIGSEGV → 139.
130Ctrl+C during recovery.
2Invalid flags or config — before your command runs.

Config precedence: flags > .phantomrc > package.json > defaults.

claude code integration

Phantom runs in one terminal. You're chatting in another.

Three optional bridges, all reading .phantom/events.jsonl — git-excluded, capped at 200 lines, events older than 24 h ignored, nothing sent anywhere.

plugin hooks

Claude opens your next reply with 👻 phantom: npm run dev crashed 3m ago — fixed on phantom/fix-…, offers the diff and the report, then carries on with what you asked.

on your next message

status line

👻 fixing npm run dev… then 👻 fixed → phantom/fix-… in the status bar until seen.

next redraw

desktop notification

Crash detected, then fixed — with the branch name. macOS, Linux, best-effort, 4 s timeout, never delays a recovery.

instantly

# inside Claude Code
/plugin marketplace add waazy-w/claude-phantom
/plugin install phantom@claude-phantom

# ~/.claude/settings.json — 👻 in the status bar
{ "statusLine": { "type": "command", "command": "phantom-status" } }

Claude Code can't be interrupted from outside, so the chat message always lands on your next turn. Use the status line or a notification if you want to know instantly.

known limitations

Where it doesn't work.

Straight from the README, because finding this out at 2am is worse than reading it now.

exit-based detection onlySupervisors that swallow the crash — nodemon, pm2, --watch — aren't detected. Wrap the underlying command instead.
git requiredOutside a repo, phantom only passes the command through.
non-deterministicClaude may fail. The branch is marked unfixed or timeout, you're back on your branch, and the report says what was tried.
best on node/jsPatching works anywhere Claude Code can edit, but verification needs a test command and the crash heuristics are tuned for Node traces first.
windows is best-effortDeveloped and tested on macOS and Linux. Signal semantics and path matching are untested; the guard hook is skipped, leaving deny rules and the audit.
not a sandboxThe session inherits your environment variables and can run node.
redaction is pattern-basedUnusual secret formats get through.
your app sees a pipe, not a TTYSet FORCE_COLOR=1 or equivalent to keep colours.
uses your Claude billingEvery recovery is a real session on your plan or API key.
daemons keep it waitingA child that keeps stdout open keeps phantom waiting. Run foreground processes.
faq

The questions people actually ask.

Does it ever push?
No — denied tool, no network tool, no push code path, no flag.
Can it touch my .env?
No. .env and .env.* are never-touch by default, enforced as permission deny rules, by the guard hook on every call, and by a post-session audit that hard-reverts the branch on any hit. Watch your own log output though — the redactor is pattern-based.
I'm mid-change. What happens?
Phantom refuses on a dirty tree. --allow-dirty stashes a snapshot first and restores it automatically, including on Ctrl+C.
How much does it cost?
One to three headless turns plus test runs, bounded by maxIterations and maxMinutes — roughly a short interactive debugging session. maxIterations: 1 and a cheaper model give you a hard ceiling.
Can I use it in CI?
Use --dry-run: diagnosis and a proposed diff land in .phantom/reports/, with no branch and no edits. Upload .phantom/ as an artifact. Full mode works too, but the branch dies with the runner since nothing is pushed.
What's the overhead when nothing crashes?
None measurable — a child-process spawn with piped stdio and a bounded buffer. A 50 MB log flood is in the test suite.
Can I run it from inside Claude Code?
Yes. Phantom strips CLAUDECODE from the environment before spawning the headless session, so nesting works.
What can the session actually see?
Your tracked and untracked source minus never-touch globs, the redacted last 256 KiB of output, read-only git history, your package.json name and scripts, your test output, and — like any CLI — your environment variables. It can never read or write a never-touch file, push, open a PR, use the network, change branches, install packages, or commit to your branch.
get started

Leave it wrapped. Forget it's there.

You need Node 18+, git, and the Claude Code CLI logged in once. That's the whole setup.

$ npm install -g @anthropic-ai/claude-code && claude

…then pick how you want phantom itself.

$ npm install -g claude-phantom
$ phantom npm run dev

The published release. Zero dependencies, so it's a single fast install.

PHANTOM_DISABLED=1 turns it back into a pure passthrough.