Skip to main content

ccdrift

ccdrift on PyPI

A check for silent changes in Claude Code, read from your own session logs.

Claude Code keeps a transcript of every session on your machine. ccdrift reads them and tells you when something shifts that you'd otherwise only notice on your bill or your usage limits:

  • Prompt caching gets worse. Less of each new prompt is read from cache, so Claude Code resends the conversation more often. ccdrift caught a real regression this way (Claude Code 2.1.233–2.1.258, August 2026) and follows such a regression until it's fixed.
  • Tool-loop turns start missing the cache on the main thread: each miss writes the whole conversation to the cache again. In subagents, which often carry most of the spend, each day's read-back is judged instead, and a drop opens an incident.
  • Haiku appears on the main thread, where your chosen model normally answers.
  • A setting Claude Code picks changes: the main thread moves between the 1-hour and 5-minute prompt cache, or its effort level changes, including when a new model arrives with a setting of its own.
  • New sessions start with a different amount of context, from the system prompt, tools, MCP servers or CLAUDE.md.
  • Stop hooks start failing, as they can when an update changes what hooks receive.
  • Requests start failing, or responses start stopping at the token limit, far more often than on the days before.
  • A model starts thinking far more per response: thinking is billed as output, so it costs money and usage limits.
  • Claude Code stops logging a field ccdrift relies on, so a silent change can't hide as a quiet week.

Each alert names the Claude Code version that was running, and what a regression has cost: tokens re-cached, extra Haiku responses or extra thinking tokens, and quotes the matching lines of Claude Code's own release notes in the log. Claude Code keeps them in the config folder that holds the transcripts: ~/.claude/cache/changelog.md, or $CLAUDE_CONFIG_DIR/cache/changelog.md when that variable is set.

It tells you when responses think far more, but not whether they think less: in one person's logs, effort swings more from day to day than a 70% cut in thinking moves it. See docs/findings.md.

Everything stays on your machine. ccdrift reads the transcripts and keeps a state file, a log, its own history of responses and, once a failing check has notified you, the time it did (check-state.json.last-failure-notice) in ~/.ccdrift. The history holds token counts, models, versions and settings, plus each transcript's path (which includes your project folder names) and session id; no prompt or response text. Only you can read the history and the log, as with Claude Code's transcripts; a log an older ccdrift made becomes private when you run ccdrift schedule install again. If your status line passes ccdrift its JSON (see Quota samples), ~/.ccdrift/quota.jsonl keeps how much of your usage limits was used and when, with the Claude Code version and model, and nothing else from it: no session id, path, project, repository or branch. ccdrift sends nothing anywhere, unless you give it a command to run with --exec.

Install

Needs Python 3.10 or newer.

uv tool install ccdrift
# or
pipx install ccdrift

For the code that hasn't been released yet, install from the repository instead: uv tool install git+https://github.com/rkolesnichenko/ccdrift.

Then schedule the check:

ccdrift schedule install                         # every hour, with notifications
ccdrift schedule install --at 18:30 --no-notify  # once a day at 18:30, log only
ccdrift schedule status                          # installed? how did the last run go?

On macOS this adds a launchd agent. On Linux it adds a systemd user timer, or a crontab line where systemd user sessions aren't available. Installing again replaces the job, and keeps the old one when the new one can't be set up. On Windows, run ccdrift check from Task Scheduler instead.

Hourly runs let ccdrift warn about cache misses within hours, once its history holds 200 or more new prompts in the two weeks before the latest week; the full verdict still takes days. Upgrading from 0.2.0, run ccdrift schedule install again: the job 0.2.0 installed keeps running once a day until you do.

Claude Code deletes transcripts after 30 days by default. From its first run on, ccdrift keeps its own history of every response it has read, so later deletions don't affect it; the history grows by about 200 MB a year for a heavy user, one with about 2,400 responses a day, subagents' included. Each check reads its last 90 days, or its last 60 days of use when those reach further back, and back to an older incident whose cost it works out; ccdrift report and ccdrift incident list read all of it. Its first run can only see what's still on disk, and it replays what it reads, its last 90 days, or its last 60 days of use when those reach further back, day by day, as if it had run all along, so a regression from before you installed it is recorded and reported once. To give that first run more to go on, and to keep transcripts for your own digging, set this in ~/.claude/settings.json:

{ "cleanupPeriodDays": 365 }

Claude Code plugin

Claude Code can run the check itself, in place of a schedule. Install ccdrift as above, then in Claude Code:

/plugin marketplace add rkolesnichenko/ccdrift
/plugin install ccdrift@ccdrift

At each session start or resume, the plugin shows the status line's one line when something needs attention, and nothing otherwise; you see it, Claude doesn't. When the last check began an hour or more ago, it also starts one in the background, with desktop notifications and its output appended to ~/.ccdrift/check.log. The session doesn't wait for it, and sessions that start together run one check between them.

When ccdrift schedule install has set up a job, the plugin's check stands aside and the job runs every check, with the options it was installed with (--exec, --no-notify, --no-digest, --source); the plugin still shows the line. Without a schedule, nothing checks while Claude Code is closed. A session under another CLAUDE_CONFIG_DIR than the one ccdrift's history was built from doesn't start a check either: the history follows one transcript folder.

The plugin runs the ccdrift in ~/.local/bin, where uv and pipx install it, and otherwise the one on PATH. It says so when there is none, or when it is older than 0.26.0, the first to have ccdrift hook. It needs a POSIX shell, so not Windows.

When an alert arrives

Alert What it means What to do
ccdrift flag 3 of the last 4 days passed the cutoff for the cache ratio, main-thread Haiku share or subagent read-back. ccdrift opens an incident and names the version and the cost so far. ccdrift report lists the days; ccdrift report --by version compares versions. A false alarm? ccdrift incident dismiss. A regression to report? ccdrift incident draft prints an issue draft.
ccdrift: back to normal The metric has been back inside the cutoff, 3 days pooled, on 3 days in a row. Nothing. ccdrift incident list keeps the record.
ccdrift: change persists The metric hasn't recovered 30 days after the incident started. ccdrift now treats the new level as normal. Check whether you changed something: hooks, MCP servers, model.
ccdrift: past incidents found The first check replayed the history on disk day by day and found incidents ccdrift would have followed. They're recorded as if it had run all along, and one still going is flagged as well, so it arrives as what it is rather than as history. ccdrift incident list shows them; ccdrift incident dismiss for a false alarm.
ccdrift: cache misses rising Several of the latest new-prompt turns missed the cache, far above your usual rate, within the last day. Nothing yet. The daily verdict follows within a few days; ccdrift report shows the days.
ccdrift: tool-loop cache misses rising Several of the latest main-thread turns inside the tool loop missed the cache, far above your usual rate, within the last day. Nothing yet. ccdrift report shows loop misses per day; the weekly summary shows whether it lasts. If it does, ccdrift incident draft tool-loop prints an issue draft.
ccdrift: subagent cache misses rising The same, for turns inside subagents. Not sent at present: on the owner's logs no setting caught a planted rise quickly enough without false alarms (G9, 2026-09-25), so subagents get no warning; their read-back is judged by the day instead (ccdrift flag). Nothing yet. ccdrift report shows subagent misses and read-back per day; the weekly summary shows whether it lasts.
ccdrift: setting changed The cache tier or effort level a model usually gets on the main thread changed, 2 days in a row, or the one the main thread as a whole usually gets did, as when a new model arrives at a different effort; the alert then names the models it moved between. A change on one model is sent once, not again for the main thread. If you didn't change it, Claude Code's default did. If you switched models yourself for 2 days or more, that is what it saw.
ccdrift: session start changed Sessions start with at least 25% more or less context than the 10 before them, on 3 in a row. Each session is measured against its own project's recent level, so moving between projects is not a change; the alert then counts the projects ccdrift could compare, those with at least 3 sessions each side of the change, and says whether every one of them moved (Claude Code, or your global config when no new version arrived) or only some did (those projects' CLAUDE.md, MCP servers or skills; when a Claude Code version new to those sessions arrived as well, it names that too rather than choosing between them). It also counts what changed in what Claude Code logged about how the sessions started: agent types, skills and MCP tools added or removed, and how many characters that came to. The names go to the check's log only, never to a notification or --exec. The logs can't say how many of the step's tokens those characters are, and the alert says so. ccdrift report names each project's typical session start; ccdrift report --by version compares versions. A step in every project? ccdrift incident draft session-start prints an issue draft.
ccdrift: a recorded session-start change was dropped Once, after an upgrade that changed how session starts are judged: a change recorded under the old rule isn't one under the new. It goes to the log only, never a notification. Nothing.
ccdrift: hooks failing Stop hooks failed on at least half their runs on 2 active days in a row, after 2 quiet weeks. Run your hooks by hand; a Claude Code update may have changed their input.
ccdrift: hooks changed The hooks on a tool's calls stopped or started running. In one project's main-thread sessions or its subagents, the latest 3 transcripts with at least 3 calls of the tool all had a PreToolUse (or PostToolUse) hook, or none did, where each of the 10 before was the other way. Changes in one thread and direction within 2 weeks are one alert. The logs can't tell whether Claude Code stopped running the hooks or stopped logging them. Hooks scoped to an agent type or a skill run only in some transcripts, so on another machine they can read as a start or a stop; the owner's logs hold none, so G15 couldn't measure it. Agent SDK sessions are left out. If you didn't change your hooks, read the named Claude Code version's release notes, and ccdrift incident draft hooks prints an issue draft. The check's log names the projects and tools; the notification names neither a project nor an MCP server.
ccdrift: requests failing A day had at least 5 failed requests (API errors Claude Code showed, or requests it retried), at least twice the busiest of the judged days in the 2 weeks before, with at least 5 such days to compare with. Banners blaming your Mac for going to sleep are counted in ccdrift report but never alert. Usually the API or your connection, not your setup. ccdrift report shows the days; check status.claude.com.
ccdrift: responses cut short At least 5 responses stopped at the token limit (or were refused) on a day, on at least 0.5% of that day's main-thread responses and 3 times the worst share of the judged days before, a clean fortnight counting as 0.1%. The comparison leaves out the days of the same run, so a regression that starts on a quiet day is still reported. A Claude Code update may have changed the output limit. ccdrift report --by version compares versions; ccdrift incident draft cut-short prints an issue draft.
ccdrift: thinking rose A model on the main thread thought at least twice its usual number of thinking tokens per response on a day, against the median of its last 14 days with 50 or more responses that logged a count. It names the model, the level against the usual one, the Claude Code version and the extra thinking tokens so far; once per rise, which becomes the usual level if it lasts. A usual level under 100 tokens per response isn't judged. If you didn't raise the effort level, a Claude Code update may have. ccdrift report --by version compares versions.
ccdrift: Claude Code stopped logging a field A new Claude Code version logs a field ccdrift reads on under 10% of responses. ccdrift peek shows what it reads. Please open an issue.
ccdrift: Claude Code logs a field ccdrift doesn't read A Claude Code version first seen in the last 2 weeks carries a field on 90% or more of its responses that under 10% of the responses in the 2 weeks before it carried. It goes to the log, the weekly summary and ccdrift status, never a notification: a field arriving breaks nothing. Nothing. Please open an issue if ccdrift should read it.
ccdrift: Claude Code writes an attachment ccdrift doesn't read A Claude Code version first seen in the last 2 weeks writes an attachment record of a kind on 90% or more of its sessions (CLI and Agent SDK sessions judged apart, 5 or more of them) that under 10% of the sessions in the 2 weeks before it held. It goes to the log, the weekly summary and ccdrift status, never a notification: a new kind of record breaks nothing. Nothing. Please open an issue if ccdrift should read it.
ccdrift can't compute the cache metric 3 busy days had no response with cache token counts, or no prompt ccdrift recognised. Claude Code's log format has most likely changed. ccdrift peek shows the first response ccdrift finds and the fields it reads from it, with text, ids and paths shown only as their length, and each content block as its type and the size of the rest. Please open an issue with what it prints.
ccdrift couldn't read a transcript The parser failed on a transcript, so its responses are missing from every check until it reads them. It is tried again on each run. None ever has in the owner's logs. The check's log names the transcript and the error. Please open an issue with the error, not the transcript.
ccdrift found transcripts without responses A transcript of 40 lines or more held no response ccdrift recognises, so its days read as days you didn't use Claude Code. Claude Code's log format has most likely changed. Of 2,389 transcripts in the owner's logs, none that long did. As for the cache metric: ccdrift peek, and an issue with what it prints.
ccdrift: weekly summary A one-line summary of the week before, from the first run after Monday 09:00 once that week's Sunday has also ended in UTC, since days are counted in UTC. It also says when ccdrift cost withholds its dollar total because a model it can't price carries 1% or more of the window's tokens, as the long ccdrift status does from the check's latest run. Nothing. --no-digest turns it off.
ccdrift check failed The check itself stopped with an error. ~/.ccdrift/check.log has the details.

Each alert is sent once, except responses cut short: a run that deepens to 3 times the share last reported is sent again, naming the level it escalated from, and a regression that simply lasts is repeated about once a fortnight. A transcript that can't be read, or holds no response, alerts again only once a week has passed without one. A failing check is logged on every run and notifies at most once in 20 hours. Claude Code's documentation says the transcript format "is internal to Claude Code and changes between versions, so scripts that parse these files directly can break on any release", which is why the cache-metric alert exists.

Incidents

While an incident is open, its days stay out of the baseline, so a regression that lasts for weeks is still judged against the days before it began. Against a rolling baseline, the August regression looked normal again within 8 days, while 5–10% of prompt turns kept missing the cache.

The first check replays the history it reads, its last 90 days, or its last 60 days of use when those reach further back, day by day and records the incidents it would have followed, with the days it would have opened and closed them on, then sends one alert about them, but only when it found any. ccdrift replay runs the same replay on any install, over all of the history, not just the last 90 days, without recording anything or sending an alert, and says whether each incident it finds is recorded.

ccdrift incident draft prints a Claude Code issue about an incident as Markdown, ready to paste into GitHub: what changed before, during and after it, by version, the release notes that may be related and your environment, as aggregates only. A cache incident's draft also shows what a missed turn looks like.

It drafts four alerts that point at Claude Code itself the same way: session-start, hooks, tool-loop and cut-short, the latest of that kind or the one starting on the day given. A session-start draft says what changed in how the sessions started, a hooks draft which events and tools it reached; two hooks alerts starting the same day are both drafted, one after the other. Claude Code's own tools are named; your projects, skills, agents and MCP servers are counted, never named.

ccdrift incident list                               every incident, its cost and versions
ccdrift incident add cache 2026-08-16..2026-09-04   record one from before ccdrift ran
ccdrift incident close cache                        end the open one as of yesterday (UTC)
ccdrift incident dismiss cache 2026-09-14           a false alarm: its days rejoin the baseline
ccdrift incident draft cache 2026-08-18             an issue draft with the evidence, aggregates only
ccdrift incident draft hooks 2026-09-05             the same about an alert: session-start, hooks, tool-loop, cut-short
ccdrift replay                                      the incidents the check would have followed

Status line

ccdrift status --short prints one line when something needs attention, and nothing otherwise: no check yet, a failing check, no check for 3 days, a check that can't see what it judges (no cache values, transcripts it couldn't read or found no responses in, or a field Claude Code stopped logging, for 3 days from the last check that found it), an open incident, failing hooks, cache misses rising, or tool-loop cache misses rising on the main thread. A state file it can't read shows as ccdrift: can't read state.

$ ccdrift status --short
ccdrift: cache ratio down since 08-18

It reads only the state file and always exits 0, so it's cheap and safe to call from the command your Claude Code status line runs. ccdrift status shows the last run, open and recent incidents, setting changes, and other changes: session start, hooks, days without cache values, transcripts it couldn't read or found no responses in, fields that stopped or started being logged, early warnings, tool-loop warnings, failed requests and responses cut short.

Quota samples

Claude Code's transcripts don't record how much of your plan's usage limits you have used, but the JSON it hands your status line command does, on Pro and Max plans: the share of the 5-hour and 7-day limits used and when each resets. Pass that JSON on to ccdrift and it keeps a sample whenever those numbers change, at most once a minute:

input="$(cat)"
# ... the rest of your status line ...
printf '%s' "$input" | ccdrift status --short --stdin

--stdin prints the same line --short does. Each sample, one line of ~/.ccdrift/quota.jsonl (beside the state file), holds its time, the Claude Code version, the model and the limits' numbers, and nothing else from the JSON. Nothing alerts on the samples yet: a rule on how fast a limit fills for the tokens you use needs weeks of them, which is why collecting starts first. ccdrift status counts them, ccdrift compare shows the quota points used each day, and ccdrift quota what each point cost. With no samples after a few responses, your plan's limits aren't in the JSON, which Claude Code sends only on Pro and Max plans.

Alerts elsewhere

--exec runs a command through the shell for each alert, with CCDRIFT_ALERT (flag, recovered, persistent, history, early, loop, subagent_loop, setting, context, hooks, hook_coverage, failed_requests, cut_short, thinking, spawn_model, fields, blank_cache, unreadable, no_responses, digest or failed), CCDRIFT_TITLE and CCDRIFT_MESSAGE set. The new_fields, new_attachments and context_dropped alerts go to the log only and never run --exec. For example, to send alerts to ntfy:

ccdrift schedule install --exec 'curl -s -d "$CCDRIFT_MESSAGE" https://ntfy.sh/your-topic'

Anyone who knows an ntfy topic's name can read what's sent to it, so pick one that's hard to guess.

A command that fails or runs longer than 30 seconds is noted in the log and doesn't stop the check.

Commands

ccdrift check [--notify] [--exec CMD] [--no-digest] [--source DIR] [--state FILE]   what the schedule runs
ccdrift report [--days N] [--by day|version] [--json | --html FILE] [--source DIR] [--state FILE]
ccdrift cost [--days N] [--by thread|agent|skill|plugin|mcp|model|project|branch] [--json] [--source DIR] [--state FILE]
ccdrift compare --at DATE [--days N] [--json] [--source DIR] [--state FILE]
ccdrift quota [--days N] [--json] [--source DIR] [--state FILE]
ccdrift calibrate [--json] [--source DIR] [--state FILE]
ccdrift status [--short [--stdin]] [--state FILE]
ccdrift incident list [--source DIR] [--state FILE]
ccdrift incident add {cache|haiku|subagent-cache} START..END [--state FILE]
ccdrift incident close {cache|haiku|subagent-cache} [--state FILE]
ccdrift incident dismiss {cache|haiku|subagent-cache} START [--state FILE]
ccdrift incident draft {cache|haiku|subagent-cache|session-start|hooks|tool-loop|cut-short} [DAY] [--source DIR] [--state FILE]
ccdrift replay [--source DIR] [--state FILE]                           incidents the check would have followed
ccdrift peek [--source DIR]                                            the fields ccdrift reads
ccdrift schedule install [--at HH:MM] [--no-notify] [--exec CMD] [--no-digest] [--source DIR]
ccdrift schedule remove
ccdrift schedule status
ccdrift hook session-start                                             what the plugin runs
ccdrift hook check                                                     the check it starts
ccdrift --version

report covers the last 21 days by default, and every version with --by version; cost covers the last 30 days with responses; compare puts the 7 days before a date beside the 7 days after it; quota covers the last 14 days the status line sampled.

report --by version also shows each version's median session start size once it has 3 or more sessions, where automatic compaction started, up to 2 release note lines about caching, Haiku and default models, effort, the system prompt and tool definitions, hooks, or subagent models, and a sub-line under each version for why the cache missed, as Claude Code recorded it. ccdrift report also shows stop-hook runs, the models subagents ran on, what each model a subagent was asked for resolved to, and tool-loop cache misses on the main thread and in subagents, which report --by version shows as a share per version.

Each Agent call names the model it wants, its result names the model Claude Code resolved that to, and the subagent's own responses name the model that served them. The check alerts once when a spawn was resolved to another model than it asked for (an alias such as opus honoured by any Opus, a full id only by itself) or served by another model than the one resolved, judging the last 7 days, once per requested, resolved and served model and not again on each Claude Code version, so a subagent model forced on purpose alerts once and then shows in report and status; a model's "[1m]" marker is ignored, since Claude Code serves it under the plain id. An alias moving to a new model, as opus does at a release, shows in the report and never alerts.

report --json holds aggregates only: no paths, session ids or project names. It adds a miss_reasons key of counts per cache-miss reason, on the day view's window as a whole and on each version's own record in --by version.

report --html FILE writes the day view as one self-contained page: the cache ratio, Haiku share and subagent read-back drawn per day, with the days of a recorded incident on that metric shaded and the flagged ones marked, a strip of each day's z under the chart with the cutoff across it, a line saying what those marks mean, then the table and the sections the terminal prints. It has no scripts and fetches nothing when opened, so it works offline, and it names your project folders as the terminal report does. Its last line says so, since a page is easier to send on than a terminal.

ccdrift cost says where the tokens went: by default it partitions the window by thread, agent type, skill, plugin, MCP server and model; --by project or --by branch reach the two dimensions the default view leaves out. Every section accounts for all of the window's tokens, not only the attributable slice, so a response can appear in more than one section: one that carries both a skill and a plugin counts once in each. It reports and never alerts: no rule, no threshold, no cutoff turns on any number here. A branch name is scoped to its repository, so a row like main can hold several at once; a row drawn from more than one project folder says how many, which is as far as the transcripts allow, since they record a working directory and not a repository root.

Dollars appear only where ccdrift could fit a price for the model from Claude Code's own cost records. One cutoff decides every dollar figure: a total, or a bucket's, is shown once the models with no price are under 1% of its tokens between them, and withheld above that, because a partial figure would be quietly wrong while an absent one is merely unhelpful. A bucket that withholds names the model that made it, in every section and not only the by-model one, and a line under the header names the models with no price whenever anything in the window has one.

Each model is priced from its own cost records: an input, output and cache-read rate, and a web-search rate when it searched, with cache writes fixed at 1.25 times input, or 2 times for those Claude Code kept for an hour. A response logs which writes those were; a cost record doesn't, so it takes the share from its session's responses. A fit whose error summed over its records is over 1% of their total cost is refused, and so is a model with fewer than four records, five if it searched, or whose cache reads rest on fewer than two.

ccdrift cost --json withholds project and branch, exactly as report --json already withholds project folders, and lists them under a withheld key rather than dropping them without saying so. It carries the evidence behind the money as well: the residual, record count and cache-read ratio of each fitted price under priced_models, and what it could not price, with that model's share of the window, under unpriced_models.

ccdrift compare --at DATE puts the days before a change you made beside the days after it: by default the 7 complete UTC days before DATE and the 7 after, with DATE itself on neither side, since the change happened some time that day. It counts the responses cost counts, so Agent SDK sessions are left out, and it says how many. Each side shows dollars per day; dollars per main-thread prompt, subagents' spend included; the median main-thread prompt size; the median session start; the automatic compactions, with the smallest, median and largest context each began at, and those that logged no size counted apart; and, on the days your status line sampled them, the quota points used. A quota point counts every surface on your account, claude.ai included, so it can move with work this machine never saw. Dollars follow cost's rule: a day whose models with no price carry 1% of its tokens has none.

Each cell is the side's median day, then its lowest and highest day and how many days had a figure. There is no verdict: a difference inside either side's range is not evidence the change did anything, and telling a real change from that spread takes a threshold measured on real logs, which compare doesn't have. It lists the versions each side ran rather than refusing when Claude Code updated in between, so read a difference against them. compare --json holds the same aggregates, with no paths, session ids or project names.

ccdrift quota says what a point of the 7-day usage limit cost: the list price of this machine's responses in the hours your status line sampled, over the points the limit rose in those hours, day by day and over the window. It counts every response on this machine, both threads and Agent SDK sessions alike, since any of them can run on your plan, and says what share of the dollars the SDK carried. A point is counted when the highest share seen so far rises between two samples of one UTC day, so an idle session's older reading can't count twice and use between a day's last sample and the next day's first is left out, even when a lower, older reading is the first thing the status line sends that morning (one equal to the night before's can't be told from a current one, since samples carry no session). The window's rate is its dollars over its points, so a quiet day can't swing it; each day's own rate sits beside its point count. It lists any point that rose while this machine logged nothing since the limit last moved: use on claude.ai or another device, or by someone else. The rate is for all models together, since the status line carries no per-model limit, and like compare it judges nothing yet. The points count your whole account, so a --source holding only some of this machine's projects lowers the rate, and quota says so when the source isn't the default. quota --json holds the same aggregates, an empty rise as its day and points only.

ccdrift calibrate says whether the daily verdict and the early warnings work on your history as they did on the logs they were tuned on. For the cache ratio, Haiku share and subagent read-back it runs the shipped detector over the days the check reads, as the check runs it: the days of open and recovered incidents stay out of the baselines, and a flag that meets an incident ccdrift follows isn't held against the metric. A flag over an incident you dismissed is, as the false alarm you marked it; so is any other. Then it plants a regression from up to 10 starting days that meet no incident, each with 7 days before it that a baseline counts (with a value, outside the incidents it leaves out) and 3 after, since the detector can't judge a day until its baseline holds 5: on the main thread a cache drop or a Haiku rise of 5% to 70% from the start on, in subagents an extra 2% or 5% of loop turns reading only what their transcript opened with, over the start and the 3 days after, 5 times each. A plant is caught when the check would have reported it by the third day after its start, judging the history up to then; one whose flag meets an incident isn't, since the check reports no such flag. A metric passes when nothing flags and a planted 5% is caught from every start (in 90% of runs for subagent read-back), the bar the shipped settings cleared on the logs they were tuned on:

Calibration on your history, 2026-08-06 to 2026-10-14: 52 days judged on the main thread, 48 with subagent loops.

Cache ratio: PASS
  A planted 5% drop is caught from 10 of 10 starts, 2026-09-09 to 2026-10-07.
  Smallest drop caught: 5% from every start.
  Nothing flags.
  Left out as incidents ccdrift follows: 2026-08-16 to 2026-09-04.

A failure says which half failed and the smallest change caught from each start. A metric has not enough data with fewer than 11 judged days, or when no start's days fall within 14 days of each other clear of incidents, since the check reports only flags begun in the last 14 days.

It judges the two early warnings below the same way, by the bars their gates in lab/ set, on the complete days the check reads. For the cache-miss warning it works out how often to expect a false warning: the alarms a simulated history at each day's usual miss rate raises with nothing planted, per turn (a rate under 0.2%, the floor the check judges against, counts at the floor, since a few hundred turns can't tell a lower one from it), times your new-prompt turns, over the weeks the warning was on (a day with new prompts it doesn't judge, such as a cache incident's, leaves them; a day without any stays), so the same setting warns more often on a busier history. Once in 6 weeks or less passes, judged on 7 days or more; the days your own history alarmed on are listed beside it and decide nothing. A 5% miss rate planted from up to 10 starts, 5 times each, must then be caught within a median of 150 new-prompt turns. The tool-loop warning passes with no alarm on any day judged at the most sensitive usual rate, 0.2%, outside the cache incidents ccdrift follows, and a planted 2% caught in 90% of runs within a median of 300 turns. It also says on which days the check, at your own usual rate, would alarm, which needn't be the same days. Subagents' tool loops ship no setting, and it says so. On the same logs:

Early warnings, turn by turn:
Cache-miss warning: PASS
  False warnings: 0.06 a week expected at your usual miss rate (0.43% median) and 483 new-prompt turns a week, over 4.1 weeks judged, against a bar of 1 every 6 weeks (0.17).
  On 2 judged days the usual miss rate was under 0.2%, the floor the check judges against, and counts at it.
  Your history raised none.
  A planted 5% miss rate is caught in a median 41 turns, 50 of 50 runs from 10 starts, 2026-09-10 to 2026-10-14, against a bar of 150 turns.
Tool-loop warning: PASS
  No alarm on 32 days judged at the most sensitive usual rate, 0.2%.
  A planted 2% miss rate is caught in 50 of 50 runs (100%), in a median 119.5 turns, against 90% within a median of 300.
Subagent tool-loop warning: no setting ships, so subagents get no early warning.

It replays the detector a few hundred times, 13 seconds on those 52 days, changes no state and sends no alert. Session start and the other alerts aren't judged yet. calibrate --json holds days, counts, rates and sizes, with no paths, session ids or project names.

Transcripts are read from $CLAUDE_CONFIG_DIR/projects when that variable is set, otherwise from ~/.claude/projects. With --source pointed at one project's own folder, that folder is read as a single project. The state file, history and log live in $CCDRIFT_HOME, otherwise in ~/.ccdrift, readable by you alone. Schedulers don't see your shell's variables, so a schedule installed while either one is set keeps its value. Install the schedule again after moving or reinstalling ccdrift.

How it decides

A day is a UTC day everywhere in ccdrift, in what the check judges and in every day it reports; a time, and the date shown with it, is in this machine's time zone.

For each complete UTC day, ccdrift looks at main-thread responses from the Claude Code CLI (Agent SDK sessions are your own scripts and are left out) and computes:

  • the cache read ratio on turns that open with a new prompt, within an hour of the previous response and not right after a compaction;
  • the share of responses from a Haiku model.

Over subagents' tool-loop turns (defined below), on days with at least 300 of them, it computes what each turn reads back of what the response before it in its transcript had cached, at most all of it. Each turn's share of its whole input read from the cache would track the work instead: its uncached part is the new tool result.

It counts failed requests the same way, on the main thread and in subagents alike (a request a subagent made is one Claude Code made), and the responses that stop at the token limit or refuse. Those are far too rare for a usual rate (9 failed requests that count, 4 sleep banners and 2 responses cut short in the six weeks this was built on), so each day is judged against the days before it instead.

Each day is compared with the 14 days before it, leaving out the days of open and recovered incidents, using their median and spread, with the spread floored at sampling noise. A day is deviant past z = −3.0 for the cache ratio, z = +3.5 for Haiku share or z = −3.5 for subagent read-back, and a metric is flagged once 3 of any 4 days in a row are deviant. These defaults were tuned on one person's logs; ccdrift calibrate measures how small a change they catch on yours, and the research harness in lab/ how they were chosen.

Every run also follows new-prompt turns one by one with a likelihood-ratio CUSUM that tests the usual miss rate of the 14 days before the last week against 5%, the August regression's rate. It warns when the sum passes h = 5 (measured in lab/early_warning.py) within the last day, at most once a week, and not while a cache incident is open. It needs 200 or more new-prompt turns in those 14 days, so it stays quiet for the first three weeks or so of history.

A tool-loop turn is a response that doesn't open with a prompt, doesn't follow a compaction and comes within 5 minutes of the previous response in its transcript. It misses the cache when it reads less than half of what that response had cached.

Every run follows these turns one by one on the main thread with the same kind of CUSUM against the usual miss rate of the 14 days before the last week (at least 1,000 turns), against 2% with h = 3 (measured in lab/loop_cache.py). It warns when the sum passes h within the last day, at most once a week, and also while a cache incident is open. Subagents' turns get no warning: no setting measured there caught a planted rise quickly enough without false alarms, so they are judged by the day instead, as subagent read-back.

A session's start is the prompt size (input plus cache tokens) of its first response. The latest 3 sessions are compared with the 10 before them: a change is at least 25%, with each of the 3 more than 12.5% off on the same side.

Stop hooks count on days with 10 or more runs. They are failing when at least half the runs report an error on 2 such days in a row, after at least 5 such days in the 2 weeks before without that.

Thinking is judged per model, on days with 50 or more main-thread responses of which 90% logged a thinking token count: a day's mean count is compared with the median of the model's 14 such days before it (at least 5), when that median is 100 tokens or more. A day at twice the median or more is a rise, reported once and only when that day is within the last 2 weeks, or, for a check that first runs later, when the rise is still going on the model's latest day. A rise that starts within 2 weeks of the model's last raised day is the same one going on unless it is twice that day's level, so a heavy day that recurs, say every week, is reported once. The setting was measured in lab/thinking.py (G16) on one person's logs, whose 2026-09-10 rise it catches on its first day.

For each Claude Code version first seen in the last 2 weeks with 50 or more responses, a field logged on at least 90% of the responses in the 2 weeks before it and on under 10% of the new version's is reported.

Attachment records are counted per session, main thread only, CLI and Agent SDK sessions apart: for each Claude Code version first seen in the last 2 weeks with 5 or more sessions, a kind of attachment ccdrift doesn't read that is on at least 90% of them, and on under 10% of the 10 or more sessions in the 2 weeks before it, is reported once. Kinds that follow what you do, like plan mode or a hook that blocked, stay under that bar.

Claude Code records a reason on a response whose prompt did not match what it had cached. ccdrift counts those reasons in ccdrift report and in ccdrift incident draft, and no rule turns on them.

Linux notes

  • systemd user timers run only while you're logged in, unless lingering is on: loginctl enable-linger $USER.
  • The timer's units go in $XDG_CONFIG_HOME/systemd/user, otherwise in ~/.config/systemd/user.
  • The timer appends to the log with StandardOutput=append:, which needs systemd 240 or newer.
  • A timer catches up on a run missed while the machine was off; cron doesn't.
  • Which scheduler ccdrift schedule install uses depends on the session: a systemd timer when a user session answers, cron otherwise (over SSH, say). remove and status look in both, and install removes a job the other one holds, so exactly one runs.
  • Notifications use notify-send. They usually appear from a systemd timer but not from cron, so with cron, watch the log or use --exec.
  • CI installs, checks and removes a real systemd timer on Ubuntu on every push; cron is covered by tests only. Reports from other distributions are welcome.

Uninstall

ccdrift schedule remove
uv tool uninstall ccdrift        # or: pipx uninstall ccdrift
rm -rf ~/.ccdrift                # state, history and log

With the plugin, also run /plugin uninstall ccdrift@ccdrift in Claude Code.

Development

uv run --group dev --group lab pytest

License

MIT

Metadata

Release files for ccdrift 0.30.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ccdrift 0.30.0
File Size Uploaded
ccdrift-0.30.0.tar.gz 521.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ccdrift 0.30.0
File Interpreter ABI Platform
ccdrift-0.30.0-py3-none-any.whl Python 3 none any Details

Total release size: 748.0 kB

Release files / ccdrift-0.30.0.tar.gz

Download URL ccdrift-0.30.0.tar.gz
Size 521.1 kB
Tags Source
SHA-256 checksum
How to use checksums
89f32842c1c05c08332027e9eb67967cc725707e1a6fb550c87c1e41ad24a6d5
BLAKE2b-256 checksum
How to use checksums
dac0cfd8fe73096875a3a948ce4a9d4c022260f13a3abcd3159590a961048d6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / ccdrift-0.30.0-py3-none-any.whl

Download URL ccdrift-0.30.0-py3-none-any.whl
Size 226.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
13b65b262636f6f03815876392dd23f585adbbd50d27faae490158acfe7462e3
BLAKE2b-256 checksum
How to use checksums
bb628f9769d9cdb115346122b6b4bd1070e5217a2fd7a1f1b77b52efaad388c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

0.30.1

2 release files

This release

0.30.0 This release

2 release files

0.29.1

2 release files

0.29.0

2 release files

0.28.1

2 release files

0.28.0

2 release files

0.27.1

2 release files

0.27.0

2 release files

0.26.1

2 release files

0.22.1

2 release files

0.22.0

2 release files

0.21.1

2 release files

0.21.0

2 release files

0.20.1

2 release files

0.20.0

2 release files

0.19.0

2 release files

0.18.0

2 release files

0.17.6

2 release files

0.17.5

2 release files

0.17.4

2 release files

0.17.3

2 release files

0.17.2

2 release files

0.17.1

2 release files

0.17.0

2 release files

0.16.1

2 release files

0.16.0

2 release files

0.15.1

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.9

2 release files

0.13.8

2 release files

0.13.7

2 release files

0.13.6

2 release files

0.13.5

2 release files

0.13.4

2 release files

0.13.3

2 release files

0.13.2

2 release files

0.13.1

2 release files

0.13.0

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.8

2 release files

0.10.7

2 release files

0.10.6

2 release files

0.10.5

2 release files

0.10.4

2 release files

0.10.3

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.10.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page