azkhh/drift
關於這個 mod
drift
一個 Claude Code 外掛,告訴你是在推進工作,還是躲在黑暗裡打轉。
它透過 Claude Code 的外掛事件觀察工作階段,統計兩件事:真正交付的內容(提交、推送、部署、通過的測試執行),以及你只是整理的內容(工具、設定、文件和計畫的編輯)。你在推進時它會保持安靜。當你長時間沒有交付任何東西,或最近的大多數編輯都落在設定和文件上,或你一直重寫同一個檔案時,提示列上方會出現一行直白的提示。只出現一次。
它不使用模型 token,不發出網路要求,不執行程序,也不讀取檔案。它統計工具呼叫,並把總數保存在你電腦上這個外掛自己的儲存空間裡。
src/app.ts 已編輯 4 次,但沒有任何交付。你最近 20 次編輯中有 12 次是設定和文件。
需要 Claude Code 2.1.287 或更新版本(從該版本起外掛預設開啟)。
安裝
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 清單。安裝時會印出一則提示,說明其中一個選項尚未設定;它是選用的。非空值會取代內建清單,不會加在後面。
外掛事件能看見什麼,不能看見什麼
這個外掛是根據 Claude Code 2.1.287 的宣告建立。以下說明每一部分依賴什麼。
編輯。 Edit、Write、MultiEdit 和 NotebookEdit 工具呼叫會帶有路徑。只有呼叫確實落地才算編輯:失敗的呼叫(isError)或被拒絕的呼叫(deny)完全不計算。
交付。 Bash 工具呼叫會帶有命令文字,外掛會從中讀取 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:工具回報錯誤時,引擎會設定它,非零結束也算在內。在實際執行中,T=1 npm test(結束 1)以 isError: true 抵達,並被算成失敗嘗試;同一個命令以結束 0 抵達時,則算成通過的測試執行。所以外掛知道零和非零,沒有更細的資訊。送到背景的命令尚未完成,不會算成交付。
它抓不到的內容。
- 一串命令只有一個結束狀態。
git commit -m x && git push以 0 結束代表兩者都執行了。a; b和a || b可能在其中一部分失敗時仍以 0 結束;npm test | tail使用tail的狀態結束,因此失敗的測試執行若透過另一個命令管線傳遞,也會被算成通過。 - 透過
Bash完成的編輯(sed -i、echo >> file、格式化工具)不會算成這個外掛的編輯。只有編輯工具才算。 - 它不辨識的交付包括:發行腳本(
npm run release)、從 CI 進行的部署,以及你在另一個終端機完成的提交。 - 它看不到思考或打字花掉的時間。活躍時間是工具呼叫、提示和已完成輪次之間不超過 10 分鐘的間隔總和。
- 通過的測試執行算成交付,因為那是正在推進的證據。它和提交一樣,會重設交付後的計時。
近似值。「處於黑暗區域的時間」是指結束事件為黑暗檔案編輯的活躍時間。時間視窗至少要有 10 次編輯,黑暗區域的比例才會提示,所以連續 3 次設定編輯不能算出結論。間隔超過 8 小時會開始新的一輪,因此昨天的編輯不會影響今天。目標相符使用一個共用詞(轉成小寫、折疊複數,並忽略常見詞和副檔名);它讀不到意圖,只能告訴你碰過的內容沒有提到目標。
隱私
claude plugin validate 會列出模組進行的每次呼叫:時鐘、工作階段的根目錄和儲存庫、外掛自己的狀態與儲存空間,以及命令註冊。它不會呼叫 $.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 ignore。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
