azkhh/drift
关于这个 mod
drift
一个 Claude Code mod,告诉你是在推进工作,还是躲在黑暗里打转。
它通过 Claude Code 的 mod 事件观察一个工作阶段,并统计两件事:真正交付的内容(提交、推送、部署、通过的测试运行),以及你只是整理的内容(对工具、配置、文档和计划的编辑)。你在推进时它保持安静。当你长时间没有交付任何东西,或者最近的大多数编辑都落在配置和文档上,或者你不断重写同一个文件时,提示列上方会出现一行直白的提示。只出现一次。
它不使用模型 token,不发起网络请求,不运行进程,也不读取文件。它统计工具调用,并把总数保存在你机器上这个 mod 自己的存储中。
src/app.ts 已编辑 4 次,但没有任何交付。你最近 20 次编辑中有 12 次是配置和文档。
需要 Claude Code 2.1.287 或更高版本(从该版本起默认启用 mod)。
安装
claude plugin marketplace add azkhh/drift
claude plugin install drift@drift
也可以在 Claude Code 内执行:/plugin,添加市集 azkhh/drift,安装 drift。重启工作阶段,然后运行 /drift。
不安装也可以从克隆目录试用:
claude --plugin-dir ./drift
提示列会说什么
在越过以下阈值前,它什么也不说。每个阈值只在首次越过时提示一次,之后会保持安静,直到它衡量的数值降回去(一次交付会重置第一项和第三项)。
| 阈值 | 提示 |
| --- | --- |
| 自上次有任何交付后经过 45 分钟活跃时间 | 自上次有任何交付后已过 52 分钟。 |
| 最近 20 次编辑中有 60% 或更多(至少 10 次)落在黑暗区域 | 你最近 20 次编辑中有 14 次是配置和文档。 |
| 同一个文件编辑了 4 次,中间没有任何交付 | src/app.ts 已编辑 4 次,但没有任何交付。 |
这行提示会一直显示在提示列上方,直到你发送下一条提示,或直到有东西交付。交付包括提交、推送、部署,或退出状态为 0 的测试运行。
活跃时间只计算事件之间不超过 10 分钟的间隔。去吃午饭时,系统会把这段时间算作空闲,不算 drift。
/drift
| 命令 | 作用 |
| --- | --- |
| /drift | 显示今天的记录和最近 7 天的数据:活跃时间、交付次数、处于黑暗区域的时间、反复改动的文件、失败尝试、目标,以及目标已保持多久。不触发模型轮次。 |
| /drift goal <text> | 为这个项目设置一个目标(以仓库根目录为键)。记录会说明从那以后编辑过的路径或提交消息,是否有词语与目标形成有意义的匹配。 |
| /drift goal | 显示目标。/drift goal clear 删除目标。 |
| /drift off、/drift on | 停止或恢复统计这个项目。 |
| /drift reset | 清除这个项目的计数器和历史记录。保留目标以及开关状态。 |
工作与黑暗区域
当编辑路径匹配以下某个 glob 时(不区分大小写;不含斜杠的模式会匹配任意层级的文件名),这次编辑就算在“黑暗区域”中。其他一切都算作工作。
CLAUDE.md AGENTS.md **/.claude/**
*.config.* package.json tsconfig* **/.github/**
.* **/.*/** (点文件和点目录)
docs/**/*.md **/docs/**/*.md plan.md notes.md todo.md roadmap.md readme.md
*.lock *.lockb package-lock.json pnpm-lock.yaml go.sum
列表集中保存在 hooks/paths.ts。要替换它,请把插件选项 Dark patterns(/plugin configure drift@drift,或 /config 中这个插件所在的行)设置为以逗号分隔的 glob 列表。安装时会提示其中一个选项尚未设置;它是可选的。非空值会替换内置列表,而不是在列表上追加。
mod 事件能看到什么,不能看到什么
这个 mod 基于 Claude Code 2.1.287 的声明构建。下面说明各部分依赖的内容。
编辑。 Edit、Write、MultiEdit 和 NotebookEdit 工具调用会携带路径。只有调用成功落地时才算一次编辑:失败的调用(isError)或被拒绝的调用(deny)不计入。
交付。 Bash 工具调用会携带命令文本,mod 会从中读取 git commit、git push、vercel deploy、wrangler deploy、npm publish、fly deploy、netlify deploy,以及 npm test、pnpm test、yarn test、bun test、pytest、go test、cargo test、vitest 和 jest。演练运行会被忽略。复合命令会在 &&、||、; 和 | 处分开,但引号内部不会分割。
退出状态。 2.1.287 中的 Bash 工具结果没有数字退出代码。它的字段包括 stdout、stderr、interrupted、backgroundTaskId 以及其他少数字段。hook 实际能看到的是 isError:工具报告错误时,引擎会将其设为 true,非零退出也属于这种情况。在实时运行中,T=1 npm test(退出 1)以 isError: true 到达,并被计为一次失败尝试;同一个命令以退出 0 运行时,则被计为一次通过的测试运行。因此 mod 只知道零和非零,无法知道更细的状态。发送到后台的命令尚未完成,不会计为交付。
这些情况不会被捕获。
- 一条命令链只有一个退出状态。
git commit -m x && git push以 0 退出表示两步都运行了。a; b和a || b可能在其中一部分失败时仍以 0 退出;npm test | tail使用tail的状态退出,因此通过另一个命令管道传递的失败测试运行会被算作通过。 - 通过
Bash完成的编辑(sed -i、echo >> file、格式化程序)不算这个 mod 的编辑。只有编辑工具才算。 - 它无法识别的交付包括:发布脚本(
npm run release)、来自 CI 的部署,以及你在另一个终端完成的提交。 - 它看不到思考或打字所花的时间。活跃时间是工具调用、提示和已完成轮次之间不超过 10 分钟的间隔总和。
- 通过的测试运行算作交付,因为它是正在推进的证据。它和提交一样,会重置自上次交付以来的计时。
近似计算。 “处于黑暗区域的时间”是指结束事件为黑暗文件编辑的活跃时间。一个时间窗口至少需要 10 次编辑,黑暗区域占比才会提示,所以连续 3 次配置编辑不能单独下结论。间隔超过 8 小时会开始新的一轮,因此昨天的编辑不会影响今天。目标匹配使用一个共享词(统一为小写、折叠复数,并忽略常见词和文件扩展名);它无法读取意图,只能告诉你最近触碰的内容没有提到目标。
隐私
claude plugin validate 会列出模块进行的每次调用:时钟、工作阶段的根目录和仓库、mod 自己的状态与存储,以及命令注册。它不会调用 $.http、$.process、$.fs 或 $.model。每个项目会保存目标,以及最近 30 天内每天的少量计数和发生反复改动的文件名。文件路径按相对于项目根目录的路径保存。存储是 Claude Code 插件存储目录中的 JSON 文件。任何内容都不会发送到其他地方。
开发
claude plugin test . # 65 个测试,由引擎自己的宿主运行
claude plugin validate .claude-plugin/plugin.json
claude --plugin-dir . # 实时试用;目录会被监视并热重载
Claude Code 每次加载插件目录时,都会把类型声明写入 .claude-plugin/types/;该目录已被 git 忽略。npx -p typescript tsc -p tsconfig.json 会针对这些声明检查 hook 和测试的类型。
代码被拆分开来,因此每个部分都可以单独测试:
| 文件 | 内容 |
| --- | --- |
| hooks/paths.ts | 黑暗区域列表、glob 和选项 |
| hooks/ships.ts | 从命令行中找出提交、推送、部署和测试运行 |
| hooks/goal.ts | 目标及其共享词检查 |
| hooks/tracker.ts | 活跃时间、编辑窗口、反复改动、交付、每日总数和 3 个阈值 |
| hooks/receipt.ts | /drift 打印的文本 |
| hooks/mover-line.ts | 结束 /drift 的那一行 |
| hooks/register.ts | hook:tool.call、prompt.submit、turn.complete、session.start、session.end、AbovePrompt 提示列、command.run |
每个 hook 都执行固定量的工作,不扫描磁盘。在脚本运行中,引擎调试日志显示,编辑事件的 tool.call hook 会在 6 到 15 ms 内稳定完成,其中包含编辑本身的时间。
许可证
MIT,见 LICENSE。
drift 只看你的编辑。mover 会记住你说过要做什么,并将它与实际做过的事比较:moveros.dev
安装
请先查看作者 README 确认 marketplace 和插件名称;命令可能随仓库结构改变。
claude plugin marketplace add azkhh/drift claude plugin install drift
原文 / README
drift
A Claude Code mod that tells you whether you are moving or hiding in the dark.
It watches a session through Claude Code's mod events and counts two things: what shipped (commits, pushes, deploys, passing test runs) and what you only tidied (edits to tooling, config, docs and plans). It is silent while you are moving. When you have gone a long stretch with nothing shipped, or most of your recent edits went to config and docs, or you keep rewriting one file, one plain line appears above the prompt. Once.
It uses no model tokens, makes no network calls, runs no processes and reads no files. It counts tool calls and keeps the totals in the mod's own store on your machine.
src/app.ts has been edited 4 times with nothing shipped. 12 of your last 20 edits were config and docs.
Needs Claude Code 2.1.287 or later (mods are on by default from that version).
Install
claude plugin marketplace add azkhh/drift
claude plugin install drift@drift
Or from inside Claude Code: /plugin, add the marketplace azkhh/drift, install drift. Restart the session, then run /drift.
To try it from a clone without installing:
claude --plugin-dir ./drift
What the band says
Nothing, until one of these is crossed. Each one speaks once per crossing, then stays quiet until the thing it measures drops back (a ship resets the first and third).
| Threshold | Line |
| --- | --- |
| 45 active minutes since anything shipped | 52 min since anything shipped. |
| 60% or more of your last 20 edits (at least 10 edits) went to the dark | 14 of your last 20 edits were config and docs. |
| The same file edited 4 times with nothing shipped between | src/app.ts has been edited 4 times with nothing shipped. |
The line stays above the prompt until you send your next prompt, or until something ships. A ship is a commit, push, deploy or test run that exited 0.
Active time counts only the gaps between events that are 10 minutes or shorter. Go for lunch and it is idle, not drift.
/drift
| Command | What it does |
| --- | --- |
| /drift | Today's receipt and the last 7 days: active time, shipped count, time in the dark, churn files, failed tries, the goal and how long it has stood. No model turn. |
| /drift goal <text> | Sets one goal for this project (keyed by the repository root). The receipt says whether any edited path or commit message since then shares a meaningful word with it. |
| /drift goal | Shows the goal. /drift goal clear removes it. |
| /drift off, /drift on | Stop or resume counting in this project. |
| /drift reset | Clears this project's counters and history. Keeps the goal and the on/off switch. |
The work and the dark
An edit is "in the dark" when its path matches one of these globs (case does not matter, a pattern with no slash matches a file name at any depth). Everything else is the work.
CLAUDE.md AGENTS.md **/.claude/**
*.config.* package.json tsconfig* **/.github/**
.* **/.*/** (dotfiles and dot folders)
docs/**/*.md **/docs/**/*.md plan.md notes.md todo.md roadmap.md readme.md
*.lock *.lockb package-lock.json pnpm-lock.yaml go.sum
The list lives in one place, hooks/paths.ts. To replace it, set the plugin option Dark patterns (/plugin configure drift@drift, or the row for this plugin in /config) to a comma separated list of globs. The install prints a note that one option is not set yet; it is optional. A non empty value replaces the built in list, it does not add to it.
What the mod events can and cannot see
The mod is built against the Claude Code 2.1.287 declarations. Here is what each part rests on.
Edits. Edit, Write, MultiEdit and NotebookEdit tool calls carry the path. An edit counts only if the call landed: a call that failed (isError) or was refused (deny) counts for nothing.
Ships. Bash tool calls carry the command text, which the mod reads for git commit, git push, vercel deploy, wrangler deploy, npm publish, fly deploy, netlify deploy, and npm test, pnpm test, yarn test, bun test, pytest, go test, cargo test, vitest and jest. Dry runs are ignored. Compound commands are split at &&, ||, ; and |, never inside quotes.
Exit status. The Bash tool result in 2.1.287 has no numeric exit code. Its fields are stdout, stderr, interrupted, backgroundTaskId and a few more. What the hook does see is isError, which the engine sets when the tool reported an error, and a non zero exit is one. In a live run, T=1 npm test (exit 1) arrived as isError: true and was counted as a failed try, and the same command with exit 0 counted as a passing test run. So the mod knows zero from not zero, and nothing finer. A command sent to the background has not finished and is not counted as shipped.
What that does not catch.
- A chain has one exit status.
git commit -m x && git pushexiting 0 means both ran.a; banda || bcan exit 0 with one part failed, andnpm test | tailexits withtail's status, so a failing test run piped through another command counts as a pass. - Edits made through
Bash(sed -i,echo >> file, a formatter) are not edits to the mod. Only the edit tools are. - Ships it does not recognise: a release script (
npm run release), a deploy from CI, a commit you make in another terminal. - It does not see time spent thinking or typing. Active time is the sum of gaps of 10 minutes or less between tool calls, prompts and finished turns.
- A passing test run counts as a ship, because it is evidence of moving. It resets the clock since a ship like a commit does.
Approximations. "Time in the dark" is the active time whose closing event was an edit to a dark file. The dark share needs at least 10 edits in the window before it can speak, so three config edits in a row are not a verdict. A break of more than 8 hours starts a fresh run, so yesterday's edits do not count against today. The goal match is one shared word (lowercased, plurals folded, common words and file extensions ignored); it cannot read intent, it can only tell you that nothing you touched has mentioned the goal.
Privacy
claude plugin validate lists every call the module makes: the clock, the session's root and repository, the mod's own state and store, and the command registration. It makes no $.http, $.process, $.fs or $.model call. Per project it stores the goal and, for the last 30 days, a few counts per day and the names of the files that churned. File paths are stored relative to the project root. The store is a JSON file in Claude Code's plugin store folder. Nothing is sent anywhere.
Development
claude plugin test . # 65 tests, run by the engine's own host
claude plugin validate .claude-plugin/plugin.json
claude --plugin-dir . # try it live; the folder is watched and hot reloads
Claude Code writes its type declarations to .claude-plugin/types/ every time it loads a plugin folder; that folder is git ignored. npx -p typescript tsc -p tsconfig.json type checks the hooks and tests against it.
The code is split so each piece can be tested on its own:
| File | What it holds |
| --- | --- |
| hooks/paths.ts | The dark list, the globs, the option |
| hooks/ships.ts | Finding commits, pushes, deploys and test runs in a command line |
| hooks/goal.ts | The goal and its shared word check |
| hooks/tracker.ts | Active time, the edit window, churn, ships, day totals, the three thresholds |
| hooks/receipt.ts | The text /drift prints |
| hooks/mover-line.ts | The one line that ends /drift |
| hooks/register.ts | The hooks: tool.call, prompt.submit, turn.complete, session.start, session.end, the AbovePrompt band, command.run |
Every hook does a constant amount of work and no disk scan. In a scripted run the engine's debug log showed the tool.call hook settling in 6 to 15 ms for an edit, with the edit itself included in that time.
License
MIT, see LICENSE.
drift only sees your edits. mover remembers what you said you'd do and checks it against what you did: moveros.dev
