Skip to content

v0.6.0 — doctor, ls, clean, recover, and a spend ceiling

Choose a tag to compare

@waazy-w waazy-w released this 22 Aug 07:02
· 23 commits to main since this release

Four new subcommands, an environment-variable config layer, and a real spend ceiling.

Built by four agents working in parallel — each owning two new files and forbidden from touching anything shared — then attacked by a security team and a debugging team before any of it shipped. Those two teams found 14 problems in code that already passed 446 tests, including one that let a repository run arbitrary commands on your machine.

New

phantom doctor — one preflight for everything a recovery needs, before your first crash:

✅ claude binary       2.1.239 (Claude Code)
✅ claude login        logged in (claude.ai) · max
✅ git repository      /path/to/repo
✅ git history         main at 0fc52487cb
⚠  working tree        14 uncommitted change(s)
   ↳ commit or stash them, or wrap with --allow-dirty
✅ test command        npm test (from package.json "scripts.test")
⚠  notifications       terminal-notifier is not installed…
   ↳ brew install terminal-notifier
✅ claude code plugin  installed and enabled

Not being logged in is the commonest first-run failure, and it used to surface mid-recovery as a blank error line.

phantom ls — this repo's fix branches, crash captures and post-mortems, newest first.

phantom clean — prunes them. Merged branches only by default; an unmerged fix branch needs --unmerged. Merged deletions go through git branch -d, so git re-checks at deletion time and a stale plan fails loudly instead of destroying work.

phantom recover — replays a crash phantom already captured, for retrying a recovery that was refused because the tree was dirty or claude was missing. The plugin has had /phantom:recover since 0.3.0; the CLI made you crash the app again.

Environment configuration — fourteen PHANTOM_* variables, sitting between the flags and the config files. A flag is this invocation, an env var is this shell or CI job, a file is the repository's default. The FAQ recommended a CI recipe that previously required committing a .phantomrc.

--config <path>, --webhook <url>, and negatable booleans--commit, --prompt, --no-notify, --verify. Booleans are tri-state internally now: "not mentioned" has to be distinguishable from "explicitly off", or a config file that turned something on could never be overridden without editing it.

maxTokens / maxCostUsd — a real spend ceiling. maxIterations and maxMinutes bound how often phantom asks and how long it waits; neither bounds what it spends. The dollar figure is an estimate from published rates, hedged as one, and phantom never volunteers an amount unless you configured a ceiling. An unknown model — the default, since Claude Code picks — prices as the most expensive one known, because a ceiling that guesses low is passed unnoticed.

Security

A planted crash capture could run arbitrary commands. A crash JSON used to be a file phantom only ever wrote. phantom recover makes it one phantom reads, and a repository can ship one. ctx.testCommand was returned verbatim by resolveTestCommand and executed by runTests with shell: true — and was never even type-checked. Clone a repo, hit a crash, run phantom recover, and their shell ran.

The test command recorded in a capture is now discarded outright: phantom resolves it locally, so a saved file does not get to choose what executes.

git.root must be absolute. A relative "." resolved against the working directory, so it satisfied the wrong-repo check from any repository the user happened to be standing in — defeating the one guard meant to stop a capture from one checkout being replayed into another.

phantom clean could delete outside the repository. The "inside .phantom/" boundary was computed lexically, and unlink follows symlinks, so a repo shipping .phantom/reports -> ../outside produced entries that passed the check while the deletion resolved through the link. Containment is anchored on the real repository root now.

--config on a non-JSON file echoed its first bytes, because Node embeds them in its parse error. It reports the position only.

Fixed

  • The stash-restore hint could point at the wrong stash. git stash parses a numeric argument as a stack index, and a 10-character sha abbreviation is all digits about 1% of the time — so git stash apply 2358190719 silently applied stash@{0} instead of phantom's entry. Worse than failing: with a one-entry stack it succeeds. That is exactly the data loss the by-sha mechanism was added to prevent in 0.3.6, reintroduced by abbreviating for readability. Shas print in full now, and the copyable command sits outside the banner box, which was wrapping it in half.
  • phantom recover --help started a real recovery — it parsed the flag and then never looked at it, so asking for help stashed, branched, patched and spent.
  • PHANTOM_DISABLED=1 did not stop phantom recover — the check sat behind a config the CLI always supplied, so the documented kill switch was unreachable from the command line for the one subcommand that spends money.
  • A subcommand after phantom's flags was silently misrouted: phantom --verbose ls ran /bin/ls, phantom --dry-run recover died with "command not found". Both are named as the mistake they are now, with the phantom -- ls escape hatch pointed at.
  • The budget stop counted an attempt that never ran, so the banner said "stopped after 2 attempt(s)" beside a token total from one.
  • A single-segment reportDir made clean silently unable to delete crash captures — with reportDir: "reports" the captures live in a sibling directory, which the first-path-segment boundary rejected while reporting success.
  • Two crashes in the same second overwrote each other. The 1-second timestamp named both the capture and the report, so the second run replaced the first's capture and appended to its report, producing one post-mortem with two verification blocks.
  • A .phantomrc at the git root beat a package.json "phantom" field in the directory you ran from, contradicting the documented "nearest first, first hit wins".
  • phantom recover usage errors exit 2 like every other usage error, not 1.
  • phantom doctor rejects unknown options instead of ignoring every argument, and no longer orphans a shim (mise, asdf, volta, npx) whose child outlives its probe.

Notes

446 → 458 tests. Every fix has a regression test. The doctor mutation check was run by hand after the agent that wrote it stalled (5/5 caught), and the suite was run under FORCE_COLOR=1.

The last bug is the one worth repeating: a regression test written for a different problem caught it, on a platform that had nothing to do with it. A suite passing is not evidence that a feature is safe — only that it does what its author expected.

CI green on all 15 jobs (3 platforms × Node 18/20/22/24, plus pack-smoke).

Full changelog: v0.5.0...v0.6.0