Releases: rkolesnichenko/ccdrift
Release list
0.16.1
Fixes from the 2026-09-25 audit (#74)
ccdrift costno longer crashes when a session's responses log one-hour cache writes but a total of zero. The one-hour share of a cost record's writes had become infinite and the price fit raisedLinAlgError. Such writes now count as none, like a session that wrote nothing, and the share is capped at all of it. The author's logs hold no such session, so no price changes.ccdrift status(the long form) reports a state file that parses but isn't shaped as the check writes it as unreadable, and exits 1, instead of printing a traceback.--shortalready did.ccdrift schedule removeon macOS unloads the launchd agent even when its plist was deleted by hand. It used to say nothing was installed while the agent kept running the check every hour.ccdrift schedule statussays the agent is still loaded.ccdrift schedule installon macOS writes the new plist before unloading the job it replaces. If the write fails, the old job keeps running and the error is a message rather than a traceback.- The launchd plist and the systemd units are written readable by their owner only. They hold the job's command, and
--execcan carry a webhook's secret. ccdrift report --htmlrefuses to write a page it can't make private and leaves the old page as it was. A failed permission change used to be ignored and the page, which names project folders, written readable by everyone.- No parser, schema or state change: nothing is re-read or migrated after upgrading.
Tests (#74)
- New tests check that the state file is private, run
ccdrift costthrough the command line, and checkreport --jsonfor leaks on a corpus with subagents, hooks, failures, incidents and branches, in both the day and the version views. 954 tests.
0.16.0
Transcripts the check loses without failing (#71)
- New alert, "ccdrift couldn't read a transcript" (--exec kind
unreadable): the parser failed on a transcript while others read. The check used to fail only when two or more failed and none read, so a format change that broke main-thread transcripts while subagent ones still read went unreported. None of the author's 2,389 transcripts fails, so one failure alerts. - New alert, "ccdrift found transcripts without responses" (--exec kind
no_responses): a transcript of 40 lines or more held no response ccdrift recognises, which is what a renamed record type leaves. Every other guard needs responses to see anything. In the author's logs the longest transcript without a response is 19 lines. - Both alert once, and again only after a week without it. The notification carries counts and the exception type; the check's log names the transcripts.
- "ccdrift can't compute the cache metric" now fires when no response logged cache counts or no prompt was recognised. It used to fire, blaming the log format, for someone who starts a fresh session per task, since no prompt then comes within the cache's hour. Neither rule fires on the author's 37 active days.
- No parser or schema change: the first check after upgrading doesn't re-read anything.
Subagent tool-loop warning turned off (#72)
- "ccdrift: subagent cache misses rising" is no longer sent. Re-run on logs through 2026-09-24, the setting shipped since 0.13.15 caught a planted rise in a median 359 turns against its gate's 300, and none of the 36 settings measured passes (G9). Subagent tool-loop misses stay in
ccdrift reportand the weekly summary. The main-thread warning is unchanged and still passes (G8).
Gates and invariants (#72)
- Lab gates G3, G8 and G9 now judge the setting ccdrift ships rather than print the best one they found. G3 printed "PASS h=5" on 2026-09-21 while the shipped h = 4 failed, and it measured the usual rate over a 100-turn floor where the check uses 200. On the check's own floor h = 4 passes.
- A digest of the parser's output is pinned to its version, and every past store schema is kept as a fixture that must migrate to the current one, so a parser or schema change without a version bump fails the tests.
0.15.1
Cache writes are charged at their own tier (#69)
ccdrift costbilled every cache write at 1.25x input. Claude Code writes the main thread's cache for an hour, at 2x, and subagents' for five minutes, at 1.25x. In the author's logs the Opus 5 models wrote 57-59% of their cache at the one-hour tier.- Each response's writes are now charged at the tier it logged. A cost record logs only its total writes, so the price fit takes the one-hour share from the record's session. A session with no one-hour writes is fitted at five minutes, as before.
- The old fit had stayed inside its 1% error bound by moving the difference into the output rate. claude-opus-5-5 had been priced at $3.55 in and $37.16 out against its $4 / $20 list price. It now fits at $4.02 and $18.90, residual 0.02%.
- On the author's 30-day window the total rises 1.04%, the main thread 3.7%, and subagents fall 1.7%. Nothing changes in the store, the parser or the state; the next
ccdrift costrefits from the records already there. - claude-opus-5 still doesn't fit its expected cache-read rate on either of its keys. docs/findings.md says so.
Findings (#68)
- docs/findings.md records three things the logs carry that ccdrift checked and won't alert on yet. Hook run times are flat (median 47 ms); the 75-hour run the 0.15.0 entry called a wait for approval was one hook whose clock ran across a long gap. MCP server failures are real but occur in two or three episodes, too few for a threshold. Environment changes are working-directory moves.
0.15.0
Hooks that stop or start running (#65, #66)
- The first check after upgrading re-reads every transcript once (parser 11) and adds a table to the store (schema 8). A store written by 0.15.0 can't be opened by an older ccdrift.
- ccdrift now reads which tool calls got a PreToolUse or PostToolUse hook, matched by tool call within each transcript. It keeps counts only, never a hook's command, output or content.
- New alert, "ccdrift: hooks changed" (--exec kind hook_coverage): the hooks on a tool's calls stopped or started running. Each project, thread and tool is judged on its own, so moving to a project without hooks is no change.
- A change is reported once, however many projects and tools it reaches, and the first check after upgrading reports nothing already in the logs.
- The notification counts tools and MCP servers; project folders and server names go to the check's log only.
ccdrift statuslists recent hook changes. - In the author's logs, subagent tool calls started getting hook records at Claude Code 2.1.261; before it, 32,735 of them had none. docs/findings.md records it, and lab gate G15 measures the alert's settings against it.
0.14.0
Read what each session starts with (#61)
- The first check after upgrading re-reads every transcript once (parser 10) and adds a table to the store (schema 7). A store written by 0.14.0 can't be opened by an older ccdrift.
- ccdrift now reads Claude Code's records of what a session starts with, before its first response: skills, deferred tools, agent types, MCP instructions, CLAUDE.md files and the system prompt. From Claude Code 2.1.267 it also reads the tool definitions. Only names and sizes are kept, never listing, prompt, CLAUDE.md or tool text, and never a CLAUDE.md path.
The session-start alert says what changed (#63)
- When sessions start with a different amount of context, the alert now counts what changed in what Claude Code logged about how the sessions started: agent types, skills and MCP tools added or removed, and the change in characters.
- The comparison stays within one project.
- The names of what changed go to the check's log only, never to a notification or --exec.
The logs can't put that in tokens yet (#62)
- New lab gate G14 checks whether logged characters convert to tokens reliably. It fails on the author's corpus: over the 46 CLI session starts, nothing could be fitted. With Agent SDK sessions included, the fit reached 0.42 tokens a character, but the parts the alert names barely vary within a project and version. So the alert gives characters and says the logs can't convert them. docs/findings.md has the numbers.
v0.13.15
The subagent tool-loop warning moves to the setting its gate now passes at. Re-running the gate behind "subagent cache misses rising" on the corpus through Sep 23, the setting 0.13.x shipped, p1 = 0.05 and h = 5, raised 4 false alarms over 15 clean days. Of the 36 settings the gate tries, only p1 = 0.02 with h = 8 passes: no false alarm, and a planted 2% miss rate caught in 50 of 50 runs, in a median 262 turns rather than 111.5. The main-thread warning is unchanged and still passes. docs/findings.md records the whole re-run of every gate; all hold but the subagent-model gate, which still fails and backs no alert.
Also: cost --json reports a cache-read ratio above 1 as null, since it means a fit went wrong rather than that reads cost more than input; and the docs state the test count and the wording rule as they now are.
Upgrading: nothing to do. The parser and the history store are unchanged, so there is no re-read of your transcripts.
v0.13.14
One transcript the parser fails on no longer fails every check. An exception while reading a transcript, or a value the history store can't hold while writing its rows, used to fail the hourly check every time until Claude Code deleted that transcript. Such a transcript is now left out with a line in the check's log naming it and the error; its rows stay as they were, and it is read again next time, as an unreadable transcript already was.
A parser bug is not hidden as a bad transcript: when two or more transcripts fail and none reads, the check fails as before. A locked or damaged history store still fails it too.
Upgrading: nothing to do. The parser and the history store are unchanged, so there is no re-read of your transcripts.
v0.13.13
ccdrift cost can now price Claude Code's default Opus model. claude-opus-5-5 logs its responses under that name and its cost records as claude-opus-5-5[1m]. ccdrift joined a response to its price by exact name, so no response could ever meet a price, and once the model passed 1% of the window (5.6% on the owner's logs on Sep 24) cost stopped printing a dollar total, with nothing that could bring it back. A model whose cost records name only its 1M-context tier is now priced from them. A model with both keys, like claude-opus-5, keeps its plain price, and one whose plain fit is refused stays unpriced rather than borrowing the 1M price, since a response can't say which tier it ran on.
This changes no figure yet on the logs it was found on: claude-opus-5-5 has one cost record, and a model needs four, two reading the cache, before it is priced. Until then the total stays withheld, and cost names the model that is holding it back.
Upgrading: nothing to do. The parser and the history store are unchanged, so there is no re-read of your transcripts.
v0.13.12
A maintenance release. Nothing ccdrift checks, and nothing it says, changes.
All of ccdrift's wording now lives in one place, and a test keeps it there. The history store's errors, the two argument errors --days and a draft's day can give, and the release-note lines alerts log now come from texts.py, which completes the move: every module is held to having no wording of its own, including one added later. Text other programs read or write stays where it is: the SQL, the crontab marker and unit files, the page's CSS, the AppleScript, Claude Code's error banners and the words release notes are searched for. Every message moved here reads exactly as it did in 0.13.11.
Upgrading: nothing to do. The parser and the history store are unchanged, so there is no re-read of your transcripts.
v0.13.11
A maintenance release. Nothing ccdrift checks, and nothing it says, changes.
The check's log and reading the transcripts take their wording from texts.py too. Every line the hourly check writes to its log, including a failed notification or --exec, comes from there, as do the no-transcripts message, the --verbose counts and peek's headings. The banners Claude Code writes on a failed request stay where ccdrift matches them. The log, those messages and peek read exactly as they did in 0.13.10.
Upgrading: nothing to do. The parser and the history store are unchanged, so there is no re-read of your transcripts.