Watchdog test fixture, not a mod to install. first-check probe: what survives a userConfig reload ($.state, $.store, module variables, timers)

A second model reviews each step that Claude Code takes and sends it short notes while it works: nit, concern or blocker.

Claude Code 2.1.290 or later. Watchdog is a mod: a plugin whose code Claude Code runs inside your session. The npm stable channel (2.1.285 on 2026-10-06) has no mods. Below 2.1.290 the plugin shows unsupported. Desktop support starts when Claude.app bundles Claude Code 2.1.290 or later.
Check claude --version first. If it is below 2.1.290, move to the npm latest channel: npm install -g @anthropic-ai/claude-code@latest.
/plugin marketplace add matteoantoci/claude-plugins
/plugin install watchdog@matteoantoci-plugins
Then run /watchdog on (reviews are off until you do) and ask Claude for a small change. The note shows as a watchdog: [concern] … line in the transcript and as a card, one line with its first sentence above the prompt box. Click the card's ▸, or press ctrl+x tab and then its letter (a, b, c), to read the whole note with its watchdog, age and state; Esc gives the focus back to the prompt. Run /watchdog status to see each watchdog's reviews, notes, tokens and cost. A card names its watchdog when you run two or more.
At its right end a card shows only what needs a look: the subagent type for a note on a subagent; the state while the note has not reached Claude yet, nudge pending (the plugin starts a turn so that Claude reads it), held or aside (Claude reads it with your next prompt); nothing once it is steered (Claude reads it after its next tool result) or nudged. When Claude edited files after the review read its update, the card says outdated? N edits (the open card says may be outdated: N edits since), and Claude reads the same mark with the note. The next review of the same watchdog sees the edits and the note; when the note no longer holds, it retracts it: the card goes, and a note that waits never reaches Claude. A blocker that may be outdated and came after Claude's reply waits as held for that review before it nudges, so Claude does not go after a bug it already fixed.
plugins/watchdog/hooks/.WATCHDOG.json and WATCHDOG.md files, the session's memory files (such as CLAUDE.md), each update of the agent you work with and, when CLAUDE_WATCHDOG is set in a claude -p run, your project and local settings./watchdog on also sends one 1-token request for each model, to check that it exists. Apart from these model requests through Claude Code, the mod makes no network calls: its code never calls $.http.fetch or fetch.<config>/watchdog/dumps/ (<config> is $CLAUDE_CONFIG_DIR or ~/.claude). It keeps its notes and review state in Claude Code's session state and plugin store. In the terminal, /watchdog dump also copies the dump text to the clipboard.Read, Grep and Glob. A project WATCHDOG.json can grant no more; only <config>/WATCHDOG.json can grant other tools and mcp__* tools. Bash, Edit, Write, NotebookEdit, Agent, SendMessage, AskUserQuestion and ToolSearch are always refused. A reviewer never asks you for a permission.Agent call of a watchdog:* type) when Claude Code would ask, so no dialog or Auto-mode classifier sees it. A permission rule that denies Agent still wins.WATCHDOG.json or WATCHDOG.md sets the number of reviewers, their model, effort and instructions. In a repo you did not write, read these files before /watchdog on. Once on, /watchdog status lists each watchdog with its model, effort and file.opus with medium effort by default (in the demo: 3 reviews, 37.2k tokens, $0.07). The built-in "You should know" mod, when on, runs its own side agent too; turn it off in /plugin to pay for one only./watchdog off stops reviews for this session. /plugin uninstall watchdog@matteoantoci-plugins removes the plugin./plugin (Installed, Watchdog, Configure options) or /config: onByDefault (default false) turns reviews on in each new interactive session. immuneTurns (0 to 5, default 3) is the number of turns after a nudge before the next nudge for a concern; a nudge is a turn that the plugin starts so that Claude reads a note that came after its reply./watchdog status shows both counts, for example nudge 1/1 · blocker 0/2./watchdog or /watchdog status: each watchdog's state, reviews, notes, tokens and cost, and the session totals. For a state such as halted, see docs/failures.md./watchdog on and /watchdog off: turn reviews on or off for this session./watchdog dump and /watchdog dump raw: write the review log to a file (raw adds the review prompts).A WATCHDOG.json in your project or in ~/.claude sets the watchdogs. The load order, every key and the tool grants are in docs/configuration.md. This file adds a second reviewer to the default one:
{ "watchdogs": [{ "name": "default" }, { "name": "security", "model": "sonnet", "effort": "high" }] }
claude -p needs CLAUDE_WATCHDOG=on and has no nudge and no cards: see docs/headless.md.plugins/watchdog/hooks/prices.ts); a model not in it shows $?.npm install sets up the tools and the pre-commit hook. npm run check runs the 6 checks of pre-commit and CI: rules, fmt:check, lint, typecheck, validate and test. See docs/plugin-dev.md. Before a release and before a bump of the pinned Claude Code version, run the live probe by hand, on a real model and login: scripts/live-probe/README.md.
Apache-2.0
hooks/reload.mjs 75 lines1// First check 11 (spec §16.4, §14.6), adapted from prototypes/first-checks/reload. `/fcreload arm` sets a module
2// variable, a `$.state` value and a `$.store` value, and starts a `$.clock.after` timer and a `$.clock.every` timer.
3// The probe then edits this plugin's `userConfig` in ~/.claude/settings.json, which reloads the module. The new
4// instance logs at `session.start` what it finds (the options, the module variable, the state and the store), starts
5// a control timer, and deletes the store key. Each instance logs to its own file `fc-reload-<instance>.jsonl`, so two
6// instances never overwrite each other.
7const INSTANCE = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
8const FILE = `__LOG__/fc-reload-${INSTANCE}.jsonl`;
9// A key of this run only: `$.store` is kept between sessions.
10const STORE_KEY = '__STOREKEY__';
11const STATE = { plugin: 'fcreload', key: 'mark' };
12const AFTER_MS = 12_000;
13const EVERY_MS = 500;
14const MAX_TICKS = 60;
15const CONTROL_MS = 3000;
16const lines = [];
17let writing = null;
18let counter = 0;
19let options = null;
20
21const log = ($, ev, data = {}) => {
22 lines.push(JSON.stringify({ t: Date.now(), inst: INSTANCE, ev, ...data }));
23 const text = `${lines.join('\n')}\n`;
24 writing = (writing ?? Promise.resolve()).then(() => $.fs.write(FILE, text)).catch(() => undefined);
25 return writing;
26};
27
28const errorText = (error) => String(error?.message ?? error);
29
30export const register = (on, given) => {
31 options = given ?? null;
32 // §14.6: at load, read what survived; a reloaded instance finds the state the old one set.
33 on('session.start', async ($, e, next) => {
34 const result = await next(e);
35 const state = await $.state.get(STATE).then(
36 (read) => ({ value: read.value ?? null, version: read.version }),
37 (error) => ({ error: errorText(error) })
38 );
39 const store = await $.store.get(STORE_KEY).then(
40 (value) => ({ value: value ?? null }),
41 (error) => ({ error: errorText(error) })
42 );
43 await log($, 'start', { options, counter, state, store, hasSource: 'source' in (e ?? {}) });
44 await $.command
45 .register({ name: 'fcreload', description: 'first-check 11 reload probe: arm', immediate: true })
46 .catch((error) => log($, 'command.error', { error: errorText(error) }));
47 if (state.value) {
48 // §14.6: a timer the new instance sets fires.
49 $.clock.after(CONTROL_MS, () => {
50 log($, 'control.fire');
51 });
52 await $.store.delete(STORE_KEY).catch(() => undefined);
53 }
54 return result;
55 });
56 on('command.run', { command: 'fcreload' }, async ($) => {
57 counter = 7;
58 await $.state.set(STATE, { inst: INSTANCE, at: Date.now() });
59 await $.store.set(STORE_KEY, { inst: INSTANCE });
60 $.clock.after(AFTER_MS, () => {
61 log($, 'after.fire');
62 });
63 let ticks = 0;
64 const every = $.clock.every(EVERY_MS, () => {
65 ticks += 1;
66 log($, 'every.tick', { ticks });
67 if (ticks >= MAX_TICKS) {
68 every.cancel();
69 }
70 });
71 await log($, 'armed', { counter, afterMs: AFTER_MS, everyMs: EVERY_MS });
72 return { text: `fcreload armed ${INSTANCE}` };
73 });
74};
75types/index.d.ts 8 lines1// State contract of the first-check 11 reload probe (prototypes/first-checks/reload/mod/types).
2export type FcReloadMark = { inst: string; at: number };
3declare module 'claude-code' {
4 interface PluginState {
5 fcreload: { mark: FcReloadMark };
6 }
7}
8