hughescr/claude-code-config/tree/develop/my-plugins/mods-probe
mods-probe
一個 Claude Code hooks-module 旗標探針,會記錄每個工作階段的 hooks 模組是否開啟,並校正推斷出的關閉狀態。
關於這個 mod
mods-probe
這是一個追蹤 Claude Code hooks 模組(「mods」)旗標的探針。只有當伺服器端 GrowthBook 旗標
tengu_plugin_hooks_modules 對該程序為 ON 時,mods 才會載入;關閉時,所有 hooks 模組都會被靜默跳過。
這個外掛把這種沉默變成證據。
在 session.start 時,它的模組會做兩件事:
- 寫入一行轉錄:
mods-probe: hooks modules are ON in this process (logged to ~/.claude/mods-flag.log)。 這裡選擇轉錄列($.ui.log)而不是 toast:toast 會持續 4 s,啟動時可能錯過或尚未繪製;轉錄列會保留, 而-p、SDK 和桌面主機也會透過ui_log收到它。 - 向
~/.claude/mods-flag.log附加一行:
2026-10-01T12:34:56.789Z session=<id|?> surface=<terminal|desktop|vscode|mobile|none> interactive=<true|false> version=<claude version|?> on
對於啟動時還沒有任何繪製位置的主機(某些 SDK/桌面工作階段),surface=none 是正常情況。
讀取日誌
旗標關閉時模組從不執行,因此探針本身只能說明「開啟」:沒有它的記錄,表示關閉或未知。「關閉」這一半來自配套的 SessionStart command hook:位於 ~/.claude 儲存庫中的 hooks/mods-flag-reconcile.ts(已接到 ~/.claude/settings.json,使用 /opt/homebrew/bin/bun 執行)。設定鉤子不受該旗標控制,因此每個程序都會執行,並附加到同一份日誌:
2026-10-01T12:34:56.000Z session=<id> source=<startup|resume|fork> pid=<claude pid|?> start
2026-10-01T12:34:56.789Z session=<id> surface=terminal interactive=true version=2.1.287 on # this plugin
2026-10-01T12:38:10.000Z session=<id> pid=<claude pid|?> for_start=2026-10-01T12:34:56.000Z inferred off
三種記錄使用同一種語法:<ISO time> key=value... <state>,最後一個 token 是狀態(start、on、off),而且必須有 session=。規則如下:
- 每個新的 claude 程序會寫入一個
start。/clear和 compact 不會結束程序,探針的session.start也不會為它們觸發,因此不會寫入任何內容。恢復工作階段只有在新程序啟動時才會寫入start(也就是自啟動以來尚未有start的 pid)。 - 某個
start若已超過 2 分鐘,而從它之前 30 s 到之後 5 分鐘的範圍內,同一工作階段沒有on,那麼下一個程序啟動時會為它寫入一行inferred off。這一行也是防止它再次被推斷的標記。探針無法讀取 session id(session=?)的工作階段所寫的on,會計入它附近的任何start。 - 因此,
on= mods 已開啟;inferred off= 程序已啟動但從未記錄on。不到 2 分鐘的start仍在等待。
報告(唯讀;也會統計已經該寫入 off 但尚未標記的 start;日期按 UTC 計算,off 計入它的 start 所在日):
bash
bun ~/.claude/hooks/mods-flag-reconcile.ts --report
# day starts on off
# 2026-10-01 6 4 2
# total 6 4 2
# latest: off at 2026-10-01T12:34:56.000Z
# last transition: on -> off at 2026-10-01T12:34:56.000Z
原始檢視(只有 on/off 列會計數;start 列是分母):
bash
tail -n 20 ~/.claude/mods-flag.log
grep ' on$' ~/.claude/mods-flag.log | cut -dT -f1 | sort | uniq -c # on-sessions per day
grep ' off$' ~/.claude/mods-flag.log | cut -dT -f1 | sort | uniq -c # inferred offs per day (by inference time)
限制
模組在工作階段執行時重新載入(熱重載、啟用)會再次觸發 session.start,因此一個工作階段偶爾會記錄兩次 on。每次 on 時,探針會透過讀取-修改-寫入把檔案裁切到最新的 2000 列,因此鉤子剛好在此刻附加的 start 可能遺失(沒有問題:遺失的 start 永遠不會產生 off)。兩個探針同時寫入時,可能會遺失一個 on,這會顯示成錯誤的 off。
除了透過原子重新命名裁切過大的檔案(超過 1 MiB,而且只在 mods 關閉夠久、探針沒有裁切時),鉤子不會重寫檔案。如果探針回報的恢復工作階段 id 與鉤子看到的不一致,恢復的程序可能看起來像 off。如果找不到 claude pid(沙盒化的 ps),會記為 pid=?。
用以下命令交叉檢查快取的旗標值:
jq '.cachedGrowthBookFeatures.tengu_plugin_hooks_modules' ~/.claude.json。
載入方式
它從 craigs-claude-plugins 市集安裝,並在 ~/.claude/settings.json 的 enabledPlugins 中啟用。這個設定涵蓋這台機器上任何目錄中的每個工作階段:CLI(不論是否使用 claude-smart.sh)和共用使用者設定的桌面應用程式都適用。claude-smart.sh --plugin-dir 不會使用:它只按專案類型載入外掛,從不到達桌面應用程式。
編輯模組後,先在 .claude-plugin/plugin.json 中增加 version,然後執行
claude plugin marketplace update craigs-claude-plugins && claude plugin update mods-probe@craigs-claude-plugins
並重新啟動工作階段。另請參閱 my-plugins/MODS-ACTIVATION.md。
檢查
bash
claude plugin validate my-plugins/mods-probe
DISABLE_GROWTHBOOK=1 claude plugin test my-plugins/mods-probe # one-off only: never put this in a launcher or settings
安裝
請先查看作者 README,確認 marketplace 與外掛名稱;指令可能隨儲存庫結構而變動。
claude plugin marketplace add hughescr/claude-code-config claude plugin install mods-probe
原文 / README
mods-probe
A flag tracker for Claude Code hooks modules ("mods"). Mods load only while the server-side GrowthBook flag
tengu_plugin_hooks_modules is ON for the process; while it is off, every hooks module is silently skipped. This
plugin turns that silence into evidence.
On session.start its module does two things:
-
Writes one transcript line:
mods-probe: hooks modules are ON in this process (logged to ~/.claude/mods-flag.log). A transcript line ($.ui.log) was chosen over a toast: a toast lasts 4 s and can be missed or not yet drawn at launch, while the line persists, and-p/SDK/desktop hosts receive it asui_log. -
Appends one line to
~/.claude/mods-flag.log:2026-10-01T12:34:56.789Z session=<id|?> surface=<terminal|desktop|vscode|mobile|none> interactive=<true|false> version=<claude version|?> onsurface=noneis normal for a host that draws nowhere yet at start (some SDK/desktop sessions).
Reading the log
The module never runs while the flag is off, so the probe alone can only say "on": silence from it means off or
unknown. The "off" half comes from a companion SessionStart command hook, hooks/mods-flag-reconcile.ts in
the ~/.claude repo (wired in ~/.claude/settings.json, run with /opt/homebrew/bin/bun). Settings hooks are not
gated by the flag, so it runs in every process and appends to the same log:
2026-10-01T12:34:56.000Z session=<id> source=<startup|resume|fork> pid=<claude pid|?> start
2026-10-01T12:34:56.789Z session=<id> surface=terminal interactive=true version=2.1.287 on # this plugin
2026-10-01T12:38:10.000Z session=<id> pid=<claude pid|?> for_start=2026-10-01T12:34:56.000Z inferred off
One grammar for all three: <ISO time> key=value... <state>, the last token is the state (start, on,
off), session= is required. The rules:
- Each new claude process writes one
start./clearand compact keep the process and the probe'ssession.startdoes not fire for them, so they write nothing. A resume writes astartonly for a new process (a pid with nostartsince it launched). - A
startolder than 2 minutes with noonfor the same session from 30 s before to 5 minutes after it gets oneinferred offline on the next session start in any process. That line is also the marker that stops it being inferred again. Theonof a session whose id the probe could not read (session=?) counts for any start near it. - So
on= mods were on;inferred off= the process started and never loggedon. Astartyounger than 2 minutes is still pending.
Report (read-only, also counts starts that are due an off but not yet marked; days are UTC, an off counts on its
start's day):
bun ~/.claude/hooks/mods-flag-reconcile.ts --report
# day starts on off
# 2026-10-01 6 4 2
# total 6 4 2
# latest: off at 2026-10-01T12:34:56.000Z
# last transition: on -> off at 2026-10-01T12:34:56.000Z
Raw views (only on/off lines count; start lines are the denominator):
tail -n 20 ~/.claude/mods-flag.log
grep ' on$' ~/.claude/mods-flag.log | cut -dT -f1 | sort | uniq -c # on-sessions per day
grep ' off$' ~/.claude/mods-flag.log | cut -dT -f1 | sort | uniq -c # inferred offs per day (by inference time)
Limits. A reload of the module while a session runs (hot reload, enable) raises session.start again, so a
session can occasionally log on twice. The probe trims the file to its newest 2000 lines on each on, by
read-modify-write, so a start the hook appended during that instant can be lost (harmless: a lost start never
produces an off). Two probes writing in the same instant can lose an on, which would show as a false off.
The hook never rewrites the file except to trim an oversized one (over 1 MiB, only when mods have been off long
enough for the probe not to trim), by an atomic rename. If the probe does not report the resumed session's id the
way the hook sees it, a resumed process can look like an off. A claude pid that cannot be found (a sandboxed
ps) is logged as pid=?.
Cross-check the cached flag value with jq '.cachedGrowthBookFeatures.tengu_plugin_hooks_modules' ~/.claude.json.
How it loads
Installed from the craigs-claude-plugins marketplace and enabled in ~/.claude/settings.json
(enabledPlugins), which covers every session on this machine in any directory: the CLI (with or without
claude-smart.sh) and the desktop app, which share user settings. claude-smart.sh --plugin-dir is not used: it
only loads plugins by project type and never reaches the desktop app.
After editing the module, bump version in .claude-plugin/plugin.json, then
claude plugin marketplace update craigs-claude-plugins && claude plugin update mods-probe@craigs-claude-plugins
and restart sessions. See my-plugins/MODS-ACTIVATION.md.
Check it
claude plugin validate my-plugins/mods-probe
DISABLE_GROWTHBOOK=1 claude plugin test my-plugins/mods-probe # one-off only: never put this in a launcher or settings