Skip to content

v0.3.6 — data loss, a pipe hang, worktrees, and two redaction leaks

Choose a tag to compare

@waazy-w waazy-w released this 22 Aug 03:28
· 27 commits to main since this release

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. .git is a file there, so ensureExcluded threw ENOTDIR, swallowed it, and never excluded .phantom/ — leaving git status permanently dirty and every crash refused with "uncommitted changes". Now resolved through git rev-parse --git-common-dir.
  • git stash pop took whatever was on top of the stack. A stash pushed by another shell, a git 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. resetHard and checkout return booleans that were discarded, so a reset blocked by a stale index.lock was 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 claude process, 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. auth is one of the sensitive key names, so the generic KEY=value rule 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. redact would 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-status and the guard hook called process.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' + stderr and 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