Releases: waazy-w/claude-phantom
Release list
v0.6.1 — three event-log findings the audit left open
Three event-log findings from the original audit that were never actually fixed. The audit was reported closed while these were still open; two of them lose data.
A torn last line swallowed the next event
appendFileSync concatenates onto whatever is already there, so a log whose final line lost its newline — a writer killed mid-write, a full disk — merged the next event into the broken one and left both unparseable.
The crash simply never reached Claude or the status line, and the log did not self-heal until the next trim. Proven in a sandbox: the event vanished entirely, leaving one event where there should have been two.
The tail is checked and healed before appending now. Losing the torn record itself is the unavoidable cost of the tear; losing the next one was the bug.
One unreadable line replayed every event the user had already seen
The cursor was a bare event id, and findIndex returning -1 was treated identically to "never acknowledged anything" — so if the cursor's own line became unparseable while its neighbours survived, the entire retained log came back as unread: a 200-event briefing dumped into the next prompt, and (+199) on the status line. Measured at 39 already-seen events replayed.
The cursor records its timestamp now, so a missing id degrades to "newer than the acknowledged time" instead of collapsing to "everything". The old single-field format is still understood, so an existing cursor keeps working.
The plugin ships its own copy of the reader and moved with it — it runs without src/ on disk, so the two have to agree on the cursor format or the plugin replays everything.
Getting the test right took three attempts
Worth recording, because the first two both reported "fixed" against the unfixed code.
When a cursor falls out of a trimmed log, the events it acknowledged have usually fallen out too — so there is nothing left to replay and the bug does not show. A scenario built that way passes either way round. Only a single corrupted line with intact neighbours discriminates: 39 replayed versus 0.
Two vacuous tests would have shipped without a mutation check on each attempt.
Documentation
The "not a sandbox" note now says what the lexical guard actually cost. It described the guard as lexical without conveying the consequence: an audit found four ways past it in a single afternoon — a recursive grep that names no path, git show HEAD:.env, a redirect written without a space, and a shell glob the matcher expanded differently from the shell.
All four are fixed and pinned by tests. The README now says plainly that a lexical guard is a speed bump, that a fifth way probably exists, and that the real backstops are the ones which do not depend on parsing a command correctly: branch isolation, the post-session audit, and the no-push rule.
Known limitation
One cursor per repository. With two Claude Code windows open on the same repo, whichever prompts first consumes the crash notification and the other never sees it.
A per-session cursor would need matching state in the plugin's own copy of the reader and a pruning story for abandoned sessions. Recorded here rather than half-built.
Notes
458 → 461 tests. CI green on all 15 jobs.
Full changelog: v0.6.0...v0.6.1
v0.6.0 — doctor, ls, clean, recover, and a spend ceiling
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 stashparses a numeric argument as a stack index, and a 10-character sha abbreviation is all digits about 1% of the time — sogit stash apply 2358190719silently appliedstash@{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 --helpstarted a real recovery — it parsed the flag and then never looked at it, so asking for help stashed, branched, patched and spent.PHANTOM_DISABLED=1did not stopphantom recover— the check sat behind aconfigthe 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 lsran/bin/ls,phantom --dry-run recoverdied with "command not found". Both are named as the mistake they are now, with thephantom -- lsescape 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
reportDirmadecleansilently unable to delete crash captures — withreportDir: "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
.phantomrcat the git root beat apackage.json"phantom" field in the directory you ran from, contradicting the documented "nearest first, first hit wins". phantom recoverusage errors exit 2 like every other usage error, not 1.phantom doctorrejects 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
v0.5.0 — guard bypasses, resource leaks, and every untrue doc claim
Closes the rest of the audit: the guard bypasses, the resource leaks, and every documentation claim that was not true.
Four ways past the guard
The path checks are lexical — they can only refuse a path that literally appears in the command line. So these read every file in the repo while naming none of them:
grep -rs . . git log -p
git show HEAD:.env find . -exec cat {} +
tar cf - . | base64 git cat-file -p HEAD:.env
Bash(grep *), Bash(git log *) and Bash(git show *) are all on the allowlist. In a sandbox repo, grep -rs . . printed the AWS key and git log -p printed it out of history.
The fix is scope-aware rather than a flat ban: a recursive search is refused only when the directory it would walk actually holds never-touch files, and git log -p only when the repo actually tracks one. grep -rn TODO src still works, because refusing it would buy no safety and push the session toward worse tools.
Three more:
- A redirect without a space defeated the tokenizer.
<and>were not split characters, socat<.env,cat 0<.envandecho pwned>.envproduced one token that matched no glob and no path. The spaced forms were caught all along, which is what made the gap easy to miss — and the write form destroyed a gitignored.envoutright. - Bash could read outside the repository.
checkFilehard-denied an escaping path from the first release;checkBashonly glob-tested them, socat /etc/passwd,cat ~/.ssh/id_rsaandcat ~/.aws/credentialswent through the one tool that can ignore the prompt's "work only inside the repository".~never resolved either, so it was missed twice over. - Bracket globs matched nothing in either direction.
expandGlobcompiled them with the never-touch matcher, which escapes[and], so.[e]nvmatched neither the file on disk nor the.envrule. The guard allowed it and the shell then expanded it.
And reportDir was validated only as "a non-empty string" while being interpolated into the Windows guard-hook command line, where arguments are quoted but not escaped — a .phantomrc reading .phantom/reports" & calc & " ran calc on every PreToolUse hook.
Resource leaks
- Ring-buffer memory was driven by the number of writes, not their size. Every write became its own Buffer and the index array grew to twice the live chunk count before compacting, so an unbuffered child — a spinner, a progress bar, anything calling
write()per character — cost about 130 bytes of heap per retained byte: 30 MB of heap and 255 MB of RSS for a 256 KB tail. Small writes are coalesced into blocks now; the same workload costs 0.3 bytes per byte. The shipped memory test only pushed 64 KB chunks, so it never saw this. - The tail could start mid-character. Eviction cuts on a byte boundary, so decoding produced U+FFFD at the head of the tail for any non-ASCII output, and that flowed into the crash JSON, the prompt and the report.
- Every successful
phantom npm run devrecovery orphaned a process tree.spawnSync'stimeoutsignals the direct child only — npm, not the server it started — which kept running and kept its port, so the user's next realnpm run devfailed withEADDRINUSEand nothing pointed at phantom. This is the documented success path: "still running counts as fixed" means the timeout fires every time a long-lived command is repaired. git clean -fddestroyed untracked work. Phantom tells you your branch is untouched, which invites you to keep working, and there is one working tree — so a file you create during a run looks exactly like one the session created, and Ctrl+C deleted it with no reflog to recover from. Untracked files are rescued into a stash first now, and phantom prints the command that brings them back.
Also fixed
- The status line claimed to be fixing crashes phantom had refused:
announceCrashran before the refusal check, so a declined crash still logged an event, and since no recovery event follows, the status line showed "fixing …" for twenty minutes while the plugin briefed Claude to look for a fix branch that was never created. - Reports were written non-atomically, so a reader could catch a half-written file and a Ctrl+C inside the write destroyed the post-mortem.
- Banner borders were misaligned wherever an emoji appeared — which is every status phantom prints — because width was measured in UTF-16 code units rather than terminal columns.
- Every run printed its outcome twice: the banner, then the identical sentence again underneath it.
Added
keepReports (default 50). Nothing pruned .phantom/crashes/ or .phantom/reports/, and each crash JSON carries the full context up to ringBufferBytes. The newest are kept; 0 keeps everything. Note this deletes files by default.
A pack-smoke CI job on all three platforms, pinned to Node 18.0.0 rather than the floating 18 that resolves to 18.20.x. It packs the tarball, installs it into a path with a space and a non-ASCII character, asserts every runtime file dependency resolves from what files actually shipped, and runs phantom inside a git worktree. Those are the classes the existing matrix structurally cannot catch — and they are exactly how plugin/ went missing from the tarball for four releases.
Documentation
Every claim below was false, stale, or unverifiable against the code:
- "the redacted last 256 KiB of output" — the session sees the last 200 lines capped at 24 KiB. 256 KiB is what phantom retains. Off by roughly 10× on the tool's central promise.
- The example banner and post-mortem were fabricated — wrong header, wrong row order,
merge/discardinstead ofaccept/reject, and a three-column table phantom has never emitted, all labelled "fromexamples/crash-demo". Both replaced with output captured from a real run. - The README contradicted itself on the macOS notification permission;
.phantomrcis read from the working directory first and not merged with the root one;--max-turnsis a third hard cap nothing mentioned; exit codes 126, 127, 129 and 143 were missing; the allow and deny tables were both incomplete while reading as exhaustive. - The site still advertised a Windows guard hole that 0.3.5 closed, and printed a
.phantomrcpanel that omittedverifyCommandand showedtestCommandat a default it does not have.
Notes
325 → 339 tests, every fix mutation-checked against the 0.4.0 behaviour, and the suite run under FORCE_COLOR=1 as well. CI green on all 15 jobs.
Full changelog: v0.4.0...v0.5.0
v0.4.0 — the event-log write race and --dry-run's missing rails
The second half of the audit that produced 0.3.6 — the two findings held back because they change behaviour rather than only fixing it.
Concurrent crashes destroyed the event log
appendEvent read the whole file, concatenated, and wrote it back, with no lock. Two phantom-wrapped commands crashing at once — a monorepo, npm-run-all -p, a CI matrix, two terminals — had each writer read a snapshot the other was mid-truncate on and write that shorter version back as the authoritative log.
Not torn lines. The file simply shrank: six writers × ten events against a full log lost 46 of the 60.
Every write is now a bare O_APPEND, which is atomic against other appenders. The cap is enforced separately, under an O_EXCL lock, by writing a sibling and renaming.
Two related fixes came with it:
- Readers could catch the log or the cursor mid-write. Rewriting in place left a window where a concurrent reader saw an empty or truncated file — about 1% of reads under load, and
phantom-statusruns on every status-line render. An empty cursor is the worse of the two: it replays the entire log as unread. Both are now write-and-rename, including the plugin's own copy ofmarkRead. - One crash could inject 200 KB into every prompt.
erroris a line of the crashed program's own output and nothing bounded it, so a minified bundle or a single-line JSON blob went verbatim into the log and from there intoadditionalContexton every Claude Code prompt in that repo — roughly 50k tokens of your context window per event.error,commandandmessageare now clamped, with the truncation visible rather than silent.
--dry-run did not restrict Bash, and never checked what happened
The file tools refused every write under --dry-run from the first release. checkBash had no dry-run branch at all, so echo patched > src/app.js, sed -i and tee went straight through.
Dry run is the worst place for that gap, because it creates no branch. Those writes landed on your own checked-out branch with nothing to roll them back — while the banner said "nothing changed", the report said Files changed | none, and the never-touch row claimed a hard revert that had never happened.
Three changes:
- Bash writes are refused in dry run — redirects,
tee,sed -i,mv/cp/touch,mkdir,chmod. Reading and verifying still work, because that is the whole point:npm test,node --test,cat,grep,git status,git diffand2>&1are all still allowed. - The tree is measured in dry run too, against a baseline of what was already dirty when phantom started — so your own uncommitted work is never attributed to the session.
- Anything that still gets through (the
node -eescape is documented, so prevention alone is not enough) is named, undone, and reported as anerrorrather than a clean dry run.
The undo is deliberately surgical: git checkout HEAD -- <paths> for tracked files, unlink for files the session created. reset --hard would be catastrophic here — dry run takes no stash, so your own uncommitted work is sitting in the same tree. There is a test asserting exactly that: your tracked edit and your untracked file both survive while the session's writes are reverted.
Also
- A failed never-touch revert was still announced as a discard.
resetHardandcleanUntrackedreturn values were dropped, so a staleindex.lockwas enough to make phantom's strongest safety claim false while the edits stayed on disk. - The post-mortem's never-touch row no longer hardcodes "(branch hard-reverted)". It states what actually happened, which in a dry run is that there was no branch at all.
One behaviour change worth knowing
MAX_EVENTS is now a ceiling, not an exact length. The whole-file rewrite that enforces it runs only once the log crosses 1.5× the cap, so the common path stays a lock-free atomic append. Between trims the log may hold up to 300 lines rather than exactly 200.
Notes
Every fix was mutation-checked against the 0.3.6 behaviour — the concurrency test was vacuous on the first attempt, because a log below MAX_EVENTS took an append fast path and never exercised the destructive rewrite.
318 → 325 tests. The suite was also run under FORCE_COLOR=1, the mode that let a terminal-only test failure through in 0.3.6. CI green on all 12 jobs (3 platforms × Node 18/20/22/24).
Full changelog: v0.3.6...v0.4.0
v0.3.6 — data loss, a pipe hang, worktrees, and two redaction leaks
Found by an audit that ran the code where the test suite never had: behind a pipe, inside a git worktree, with a second stash on the stack, and with credentials in argv. Twelve fixes, every one with a regression test, and every test mutation-checked against the 0.3.5 behaviour.
The two worst
Following phantom's own recovery instructions destroyed your work. When phantom left you on the fix branch it skipped popping your snapshot stash — the guard read !s.onPhantomBranch, which is still true there — and then printed git stash && git checkout main. Running that put phantom's unverified patch on the branch phantom had just called untouched, lost the tree state you had, and buried your real work under a stash you were told you had already restored.
There is now a test that executes the printed advice verbatim and asserts you get your work back. Reverting the fix makes it fail on the lost edit.
phantom -- cmd | head hung forever. On EPIPE the output pump called src.unpipe(dest), which also clears flowing mode; the ring-buffer data listener does not bring it back, so the child's stdout was never drained again. A child that writes synchronously to fd 1 — most programs that are not node — then blocked on a full pipe with nothing left to settle the run. Same for | grep -q and quitting the pager.
Also fixed
- Worktrees and submodules were unusable.
.gitis a file there, soensureExcludedthrewENOTDIR, swallowed it, and never excluded.phantom/— leavinggit statuspermanently dirty and every crash refused with "uncommitted changes". Now resolved throughgit rev-parse --git-common-dir. git stash poptook whatever was on top of the stack. A stash pushed by another shell, agit pull --autostash, or a second phantom run meant phantom restored a stranger's content over your tree and reported success. The snapshot is now recorded by commit sha and resolved to its current position immediately before the pop.- Cleanup claimed "working tree restored" without checking.
resetHardandcheckoutreturn booleans that were discarded, so a reset blocked by a staleindex.lockwas announced as a successful restore. Failures now name the branch you are still on and where your work is. - Failing after the stash was taken orphaned the whole tree. Early returns left the try block without running cleanup, so your uncommitted work vanished while the final message named an unrelated cause.
- SIGHUP was not handled. Closing a terminal tab or dropping an SSH session left you on the phantom branch with a live stash, an orphaned
claudeprocess, and no output at all. Now exits 129 after restoring. - A conflicted stash pop was reported as a retryable failure. git has already written the merge and kept the entry, so "run
git stash pop" could not work. Authorization:headers were published, not redacted.authis one of the sensitive key names, so the genericKEY=valuerule matched first and treated the scheme as the secret —Authorization: [REDACTED] sk0pq7Rt...— scrubbing the one part that was never sensitive.- The crashed command's argv was never redacted.
node server.js --api-key=...went verbatim into the prompt sent to the model, the post-mortem, the crash JSON, the desktop notification and the webhook POST — the one destination that leaves your machine.redactwould have caught it; it was simply never called. - Credentials in URL query strings (
?api_key=,?access_token=), underscore-form tokens (sk_live_...), and quoted multi-word secrets are now redacted properly. phantom-statusand the guard hook calledprocess.exit()after writing. Pipe writes finish asynchronously on Windows, so the status segment could vanish and a guard denial could arrive with an empty reason. A structural test now enforces the rule across every executable that writes to stdout or stderr.- A first run without a Claude Code login reported nothing at all. The error string is built as
'' + '\n' + stderrand phantom took line 0 — the empty string — so you saw "claude ended with an error:" followed by nothing, watched the test suite run three times, and were then told the session made no changes. Claude's actual message ("Please run /login") was on the next line.
Notes
Two of the new regression tests were vacuous on the first attempt and mutation testing caught both — the SIGHUP one passed with SIGHUP removed from the handler list, because calling abort() directly bypasses registration entirely.
The first version of the corrected stash advice was itself broken: git stash pop <sha> is rejected by git (is not a stash reference). It is git stash apply <sha> now, which takes a commit and leaves the entry in place until you are satisfied.
308 → 318 tests. CI green on all 12 jobs (3 platforms × Node 18/20/22/24).
Full changelog: v0.3.5...v0.3.6
v0.3.5 — guard parity, Windows guard hook, and a plugin data-loss fix
A branch-coverage pass turned up six real bugs. All six are fixed here, and every one is pinned by a test that fails if it comes back.
Security
The guard's fallback matcher no longer under-blocks. guard-hook.js carries its own copy of the never-touch glob logic so the guard still works when never-touch.js cannot be loaded — but it was a looser reimplementation, and every gap was a silent hole. It had no {a,b} alternation, so a neverTouch of *.{pem,key} was entirely unenforced whenever the fallback was live. It also did not trim the glob, strip a leading /, or skip an empty one. It is now a faithful copy, and a parity test across the shipped defaults fails on any future divergence.
The guard hook runs on Windows. Its command carried the guard's config in a POSIX VAR=value prefix that cmd.exe cannot parse, so it was skipped there entirely. The deny rules left behind cover Read/Edit/Write/Grep/Glob but not Bash, while the allowlist grants Bash(cat *) — so cat .env was unguarded on Windows alone, and the README's claim that .env is covered was false on one of three platforms. The config now travels in a file that the hook reads from argv; an unreadable one fails closed, like every other unparsable input.
Data loss
The plugin hook could lose crash events permanently. It wrote to a pipe and then called process.exit(0), discarding everything past the buffer — one minified stack trace in error is enough. Claude Code received invalid JSON and dropped the briefing, and markRead() had already advanced the cursor, so those events were gone for good. The cursor now moves only once the bytes are actually flushed, and never when the write failed.
describeEvent threw on a command that would not coerce. Because the throw came before markRead, the cursor never advanced and every later prompt in that repo retried and failed identically. Every field is coerced safely now — and flattened to one line, since error is raw output from the crashed program and the briefing is line-oriented text.
Correctness
reproTimeoutMswas read offopts, which only ever carriescwd, so the re-run timeout was hard-wired to 30 s and every override was silently ignored. The test suite went from 45 s to 17 s.- A failed commit told you it left you on the branch "(--no-commit)" when
autoCommitwas on. It now names the actual reason.
Also
- The Claude Code plugin is documented as a first-class install path, with update instructions for both channels.
- 243 → 306 tests.
prompt.jsbranch coverage 59.8% → 98.3%;recovery.jsat 100% lines.
Install: npm install -g claude-phantom · Plugin: /plugin marketplace add waazy-w/claude-phantom
v0.3.4 — the Claude Code plugin actually loads
Fixed
- The Claude Code plugin never loaded.
plugin.jsondeclared
"hooks": "./hooks/hooks.json", but Claude Code loads that path by convention, so the
manifest registered it a second time and the whole plugin failed with "Duplicate hooks
file detected"./plugin installreported success; only the/pluginerror tab showed
it. Broken since the plugin was written and shipped in every release, because nothing in
the suite read the manifests.test/plugin-manifest.test.jsnow validates both of them:
no re-declaration of conventional paths, every referenced directory and hook script
exists, the marketplace entry resolves to the plugin, and the versions track
package.json.
v0.3.3 — verify the crash is actually gone
Fixed
--notifyproduced nothing at all on macOS 15.6, with no way to tell. A
display notificationfrom a command-line process is discarded: nothing appears,
osascriptexits 0, and the script never registers under System Settings →
Notifications -- so there is no permission to grant, and phantom cannot observe the
difference between delivered and dropped. It now warns once per run when it is on that
path and points atbrew install terminal-notifier, which ships its own bundle and does
deliver. The README previously said to allow Script Editor; that entry never appears, so
the advice was wrong.
Changed
- A crash with nothing to go on is now declined instead of recovered.
phantom node -e "process.exit(7)"spent 90 seconds and ~300k tokens to conclude nothing: no error line,
no stack trace, no file named in the output and no test command, so the session could
neither locate the fault nor tell whether it had fixed it. That is the shape of a linter
or build tool exiting non-zero, and spending a session to reliably achieve nothing is
worse than saying so. A project with a test command still gets a recovery -- the suite is
both the map and the proof -- and--dry-runstill runs, since a diagnosis without
verification is exactly what that mode is for.
Fixed
-
Phantom could report a fix that fixed nothing. Verification ran the test command and
nothing else, so a session that changed no code at all -- or changed code without
touching the crashing path -- was announced as✅ fixed · fix verified by phantomwhile
the original command still crashed with the identical error. The tests that "verified" it
were the ones already passing while the command was dying, since the bug lived in a path
they never covered. Found by running phantom against a real crash, not by the suite.Two changes. A session that changes nothing can no longer be
fixed, whatever the suite
says. And after the tests pass, phantom re-runs the command that crashed: exit 0, or
still running at the 30 s cap, is the evidence; the same failure again isunfixedwith
the exit code named. The still-running case is deliberate --phantom npm run dev
crashed on boot, and surviving past the point it used to die is exactly the proof wanted,
while waiting for an exit would hang the recovery forever.The post-mortem's verification table gains a
Crashed command re-runrow, so the
distinction between "tests pass" and "the crash is gone" is visible rather than implied.
Set"verifyCommand": falseto skip the re-run if the command has side effects you do
not want repeated. -
A false alarm on the most alarming message phantom has. The stat-snapshot audit flags
any never-touch file that changed on disk, and phantom reported "phantom cannot restore
these; inspect them now" for a tracked.envthat the hard reset put back a second
later. It now checks whether git tracks the file: only a file git never knew about -- a
gitignored.envis the usual one -- is genuinely beyond recovery. A tracked one is
still a violation and still reverts the branch, but is reported as restored, because it
was.
v0.3.2 — the merge prompt actually waits
Fixed
- The end-of-recovery prompt never waited for an answer. It read from
process.stdin, but
the recovery session is a long-lived child that takes the controlling terminal, and once
it exits the inherited stdin no longer delivers a line -- readline saw end-of-input and
closed, so the question printed and vanished and the keystroke landed in the shell
instead. The prompt now reads from/dev/tty, a fresh handle on the same terminal that
is unaffected by whatever the child did, falling back toprocess.stdinon Windows and
where there is no controlling terminal. Whether to prompt at all is still decided by
process.stdin.isTTY, so a piped stdin in a script or CI still gets no prompt even
though/dev/ttywould supply one. - The terminal is opened as a
tty.ReadStreamrather than a plain file stream, so readline
can turn off the driver's own echo. WithoutsetRawModeboth echoed and every keystroke
appeared twice.
v0.3.1 — token counts show what was new
Changed
- The token figure now shows what was actually new:
468.2k tokens (12.2k new · 456k cached). A bare total is honest but uninterpretable -- a one-iteration recovery reports
something like 468k, which reads as enormous until you know ~97% of it is the same system
prompt and the same files re-read every turn and billed as cache reads at a fraction of
the input rate.