v0.1.5
The sanitation release. Six independent security reviews of 0.1.4 were read, every
claim verified against the code, and everything confirmed is fixed here, along with
the sharpest bug of the round, which no review found.
Security
- Nothing hostile reaches your terminal anymore. Directory names, branch names and
hook text are stripped of control bytes once, at the daemon boundary, so an escape
sequence hiding in a branch name can no longer repaint the screen from inside the
status line. Width is measured in terminal columns on grapheme clusters, so a CJK
or emoji label can neither wrap the strip nor be cut through the middle of a glyph. - The salvage path for a malformed hook payload now recovers identifying fields only.
A truncated payload could previously leak nested free text, up to a tool's message
body, onto the socket and into the on-disk spool. --purgenever issues a recursive delete of the cctap directory root. It removes
exactly the children cctap created and then anrmdir, so a mistypedCCTAP_DIR
pointing at a real directory survives with everything foreign intact.CCTAP_DIR
itself now expands~, must be absolute, and is refused outright at/or at the
home directory. A keys journal that exists but cannot be read refuses the purge
instead of silently disarming that guard.- The daemon only ever unlinks its socket path after checking the path is actually a
socket, andcctap stopverifies the recorded pid is really a cctap daemon before
signalling it, so a recycled pid or a misconfigured socket path can no longer take
down or delete something that was never cctap's. - Everything cctap writes that a shell later re-parses is POSIX single-quoted: the
installed status line command, the printed keys recipes, the tmux conf line. A$,
backtick or space in a home directory no longer kills the strip or executes
path-derived text. AppleScript tty interpolations are escaped the same way, and
notification bodies are clamped before they reach any argv.
Fixed
- A resumed session no longer shows a phantom permission prompt.
SessionStartnow
resets the subagent tree and the attention fields, soclaude --resumeinside the
ended-session window cannot inherit a blocked subagent from the previous life,
which used to sort itself to jump priority one and then hold the spinner so the
session never read as your turn again for the rest of its life. - The spool is bounded by atomic rotation instead of a read-filter-rewrite, so a
concurrent append can no longer be lost and a backed-up spool no longer blows the
forwarder's time budget. Rotated segments are capped at five, oldest dropped with
a counted marker that now survives replay into the state file, where doctor reports
it. A daemon standing down because another one is serving no longer consumes and
deletes spooled events on its way out. - Each hook invocation carries a random
eidshared by its delivered and spooled
copy, and the daemon drops repeats, so the retry path can no longer double-count an
anonymous subagent and leave a session spinning forever. A subagent tree whose
start and stop events did not pair up is released after a bounded stall instead of
hiding your turn for good, and doctor says when that happened. - A daemon whose socket rebind failed retries on the next sweep instead of staying
permanently unreachable while holding the lock, and a stood-down daemon no longer
deletes the serving daemon's info file. The daemon survives exceptions in its own
side effects, drops subscribers that stopped reading before they grow its memory,
retries snapshot writes that failed transiently, retries a tty resolve that failed
once, caps captured environment values so no legal event can outgrow the line cap,
and tracks sessions whose ids collide with object prototype names. - The installer validates your settings file before it copies or writes anything, so
an aborted run really has changed nothing, binaries included. Binary copies land
atomically, backups are created exclusively with millisecond-and-pid names so no
run can overwrite the backup holding your pre-cctap settings, a symlinked
settings.json is written through rather than replaced, and a settings file that
changed mid-run aborts instead of reverting the other writer. Unknown flags exit 2
before any work, so a misspelled--dry-runcan never perform the real install.
uninstall-hooks,stopandnotifyrun even under a broken socket path, and an
uninstall with no settings file still stops the daemon and honours--purge. - A hooks entry with a shape cctap cannot merge into is preserved and reported
instead of being replaced, in every case. - A notification suppressed by the debounce window is deferred and delivered when the
window closes if the state still holds, instead of being dropped. Permission
answered fast then your turn no longer means no banner at all. - Key input on the live surfaces accepts only exact single digits, so F5, Delete,
a paste or a mouse report can no longer jump you somewhere a digit happened to
appear in the byte sequence. - The live strip keeps the last known board when the daemon link drops and marks it
daemon down, state unknown, instead of repainting as a calm empty strip while a
permission prompt may be pending.topclamps to the terminal viewport, so thirty
sessions on twenty rows no longer scroll the screen into an unreadable smear, and
both surfaces restore the terminal on every way out, including crashes, hangup and
Ctrl-Z. - GNOME key setup captures the prior keybinding values before writing and restores
them on undo, journals partial failures so nothing orphaned becomes invisible, and
tmux setup only treats a binding as its own when it runs cctap's exact jump line.
The macOS clipboard warning prints before the clipboard is replaced, not after. - The macOS socket path limit is 103 bytes, not 104. The old constant allowed a path
that the kernel truncated by one byte.
Added
- Reduced motion:
CCTAP_MOTION=0or"motion": falsefreezes the spinner and stops
every animation ticker, in the status line, the live strip and top alike.NO_COLOR
now also beats the installer-forced color on the status line. - Release engineering: CI proves the build byte-identical twice over, every workflow
action is pinned to a commit SHA, the release workflow refuses a tag that does not
match the manifest, skips green when the version is already on npm, publishes
prereleases to thenexttag, verifies the published rebuild matches the tested
bundles, and creates a GitHub Release from this file's section for the tag.
Changed
cctap statussayswaitingfor the state the strip callsyour turn. One state,
one word.- Node 22 or newer. Node 20 passed its end of life in April 2026.
- Session labels that collide extend their disambiguating suffix until unique, and a
repository's detached HEAD state is labelleddetachedinstead ofHEAD. - Docs: the README quick start works as pasted, the npm page renders its images, the
install docs say exactly what is written where, PRIVACY documents every stored
field including the nine environment variables and the new rotation andeid
details, SECURITY carries a threat model, andcctap doctoris documented where
people can find it.