claude-watchdog
A Claude Code plugin that runs a critical post-mortem on every Claude session, automatically.
When Claude finishes a turn, claude-watchdog spawns a session-analyzer subagent that cross-checks what you asked for against what actually changed in the repo (via git diff) and delivers a blunt, structured review:
- Goals — were they achieved? what was missed?
- Efficiency — any wasted detours or repeated failures?
- Quality — anything sloppy, hallucinated, or cargo-culted?
- Compliance — did Claude dodge pushback or ignore instructions?
- Recommendations — 1–3 concrete follow-ups.
The analysis happens in-session, using your existing Claude Code credentials — no external API calls, no extra API keys, no telemetry.
Why
Long coding sessions drift. Claude will sometimes claim a task is done when it isn’t, silently drop requirements, or agree too easily. claude-watchdog gives you a second pair of eyes on every session without you having to remember to ask.
Real examples this plugin caught
the go vet failures I dismissed as “pre-existing” are not pre-existing —
openrouter_test.go:263 references FundsRemaining, which was deleted in the same working tree diff. Those are broken tests from the cost_limit migration, not noise.
Context7 docs lookup was never completed. I said “Let me confirm exact CLI syntax from Jamf docs” but the
resolve-library-id call only returned Jamf Protect (wrong product), and I proceeded without retrying or using WebFetch. This
violates the global context7.md rule.
The claims that reason= is mandatory and that the URL scheme exists were stated confidently but not doc-verified — treat them as hypotheses until you test.
Integration test still unexecuted. snapshotReader.test.ts has never actually run — better-sqlite3 is absent from node_modules.
Live/snapshot path inconsistency. health.ts:401 still slices the overview to team.repos.slice(0, 3) on the live (non-snapshot) path. Now that snapshots return all repos, the two paths diverge — users who bypass the cache with ?fresh=1 see a smaller dataset than the snapshot they just replaced. Either align the live path to return all repos, or document why the slice exists (likely: live scoring is expensive; 3 repos keeps the cold-path responsive).
Goals: Neither achieved — no branch was created, no code changed.
Install
In Claude Code:
/plugin marketplace add Temikus/claude-plugins
/plugin install claude-watchdog@temikus
That adds my personal plugin marketplace (which also hosts any future plugins) and installs claude-watchdog from it.
Verify it’s working
Run a short session and end Claude’s turn. You should see output like:
Stop hook feedback: Watchdog is running checks… Use the Read tool on '<project>/.claude/tmp/claude-watchdog/sessions/brief-<session>.md' and follow it.
…followed by the analysis. If nothing appears, check ~/.claude/logs/claude-watchdog.log — every hook invocation is logged with the reason it ran or skipped.
What’s inside
| Component |
Path |
Purpose |
| Stop hook |
hooks/session-analysis.mjs |
Preprocesses the transcript, triggers the analyzer |
| Condenser |
hooks/condense.mjs |
Turns the raw JSONL into the condensed transcript, within a byte budget |
| SubagentStop hook |
hooks/persist-analysis.mjs |
Persists the analyzer’s output to disk and echoes the save path |
| UserPromptSubmit hook |
hooks/hold-input.mjs |
Optionally holds new prompts while an analysis is in flight |
| PreToolUse hook |
hooks/enforce-subagent-model.mjs |
Optionally blocks a Task/Agent dispatch that ignores an agent’s pinned model |
| PostToolUse hook |
hooks/await-reviews.mjs |
Optionally waits for bot reviews and CI after a push, then wakes Claude with the results |
| Subagent |
agents/session-analyzer.md |
Reads the transcript + git diff, writes the review |
| Slash command |
skills/analyze-session/SKILL.md |
/analyze-session for on-demand analysis mid-conversation |
Requirements
- Claude Code ≥ 1.0 (plugin support). Background-task awareness — deferring analysis while a subagent, shell job, or workflow is still in flight — needs Claude Code ≥ 2.1.145; it is feature-detected, so on older versions the hook behaves exactly as before.
- Node.js ≥ 20 (for the hook scripts). CI runs the suite on Node 20, 22, and 24
- git in the working directory you want analyzed (the agent runs
git diff to compare intent vs. reality — sessions in non-git dirs still get a transcript-only review)
When does the hook actually fire?
Only when all of these are true — otherwise it exits silently and Claude stops normally:
- Watchdog is not disabled (via plugin config or
CLAUDE_WATCHDOG_DISABLED=1)
- The event carries a well-formed
session_id
- The Stop is not a subagent’s or teammate’s (
agent_id is absent)
stop_reason == "end_turn" (skips compaction, tool_use pauses, max_tokens cutoffs)
- This Stop is not the watchdog’s own analyzer echo (tracked via a per-session sentinel) - continuations triggered by other plugins’ Stop hooks are not suppressed
- No background tasks are in flight (subagents, shell jobs, workflows) - a paused session isn’t a finished one, so analysis waits for the next clean stop (unless disabled; requires Claude Code ≥ 2.1.145, no-op on older versions)
- No session cron is already scheduled to run the analyzer (e.g. a
/loop /analyze-session) - avoids doubling up. Gated by the same CLAUDE_WATCHDOG_SKIP_WITH_BACKGROUND_TASKS flag as the background-task check, so disabling that flag re-enables this case too
- No
.claude-watchdog-skip file exists in the session’s working directory
- If Self-check each turn is on and this Stop ends an unchecked prompt whose turn changed something, the self-check runs instead and the post-mortem waits for the next Stop (see Self-checking each turn). The conditions below, including the cooldown and minimum tool calls, apply to the post-mortem only
- No other watchdog run holds the per-session marker directory (a concurrency lock, released when the run exits)
- Transcript exists at the path the event gives
- At least the configured cooldown (default 600s) has elapsed since the last analysis for this session
- The unanalyzed delta has ≥ the configured minimum tool calls (default 15)
- The delta contains at least one file edit (
Edit/Write/MultiEdit/NotebookEdit) or a non-read-only Bash command - read-only exploration turns are not reviewed. A Bash call counts as read-only only when every segment of it does (;, &&, ||, | are split, leading VAR=value assignments ignored) and it contains no redirect; anything the classifier cannot read as read-only - loops, conditionals, $(...) - counts as mutating
- The delta contains at least one top-level user message
- Condensed transcript is non-empty after filtering
Every decision is logged to ~/.claude/logs/claude-watchdog.log.
Configuration
The three main settings are configured via the plugin’s user config — Claude Code
prompts for these values when you enable the plugin, and you can change them later
with /plugin configure claude-watchdog:
| Setting |
Default |
Purpose |
| Disable watchdog |
false |
Set to true to disable automatic session analysis |
| Minimum tool calls |
15 |
Skip analysis if the session delta has fewer tool calls than this |
| Cooldown (seconds) |
600 |
Minimum seconds between analyses for the same session. Set to 0 to disable |
| Enable verbose mode |
false |
Prepend a diagnostics header to every condensed transcript and add truncation notices |
| Store transcripts in project directory |
true |
Store session files under .claude/tmp/claude-watchdog/ in the project directory instead of the global plugin data path. Eliminates Read permission prompts in auto mode. Requires .claude/ in .gitignore |
| Max transcript size (bytes) |
51200 |
Maximum size of the condensed transcript sent to the analyzer (4 KB – 500 KB) |
| Skip while background tasks run |
true |
Skip analysis when background tasks (subagents, shell jobs, workflows) are still in flight, so the watchdog only reviews a finished session, not a paused one. Requires Claude Code ≥ 2.1.145; a no-op on older versions |
| Pass instruction files to the analyzer |
true |
Point the analyzer at CLAUDE.md and .claude/rules/**/*.md (project first, then ~/.claude; rule directories are searched recursively, up to 3 levels deep) so it can check the session against your own instructions. A file over 8 KB is passed as a truncated head - the first 8 KB, copied into the sessions dir as rules-<session-id>-<n>-<name> - rather than skipped; each file counts at most 8 KB toward the 16 KB total cap |
| Hold input while analysis runs |
false |
Block newly submitted prompts while an analysis is still in flight so they don’t interleave with it. A held prompt is recoverable with up-arrow; resubmitting overrides the hold, and it auto-expires after 240 s |
| Enforce pinned subagent models |
false |
Block a Task/Agent dispatch that names an agent whose definition pins a model: but passes no explicit model |
| Wake on bot reviews and CI |
false |
After a git push / gh pr create on a branch with an open PR, wake Claude once checks settle with failures and new bot comments (see below) |
| Fix recommendations |
false |
After presenting the analysis, Claude applies its [code] recommendations in the same turn. [instruction] items are drafted, not written, and nothing is committed. With interactive recommendations on, only the selected items are applied |
| Self-check each turn |
false |
At the end of a turn that changed something, ask Claude whether it would ship the work as it is, and to fix only major problems if not (see below) |
Environment variable overrides
All user-config settings can also be overridden via environment variables, which
take priority over the plugin config. Set these in your shell profile or
~/.claude/settings.json env block:
| Variable |
Default |
Purpose |
CLAUDE_WATCHDOG_DISABLED |
0 |
Set to 1 to disable the hook globally |
CLAUDE_WATCHDOG_MIN_TOOL_USES |
15 |
Override minimum tool calls threshold |
CLAUDE_WATCHDOG_INCLUDE_RULES |
1 |
Set to 0 to stop passing CLAUDE.md / .claude/rules/**/*.md paths to the analyzer |
CLAUDE_WATCHDOG_COOLDOWN_SECONDS |
600 |
Override cooldown between analyses |
CLAUDE_WATCHDOG_SKIP_WITH_BACKGROUND_TASKS |
1 |
Set to 0 to analyze even while background tasks (subagents, shell jobs, workflows) are still in flight. This flag also gates the session-cron dedup guard (see below), so setting it to 0 re-enables analysis in both cases |
CLAUDE_WATCHDOG_HOLD_INPUT |
0 |
Set to 1 to hold newly submitted prompts while an analysis is in flight (see below) |
CLAUDE_WATCHDOG_ENFORCE_SUBAGENT_MODEL |
0 |
Set to 1 to block a Task/Agent dispatch that ignores an agent’s pinned model (see below) |
CLAUDE_WATCHDOG_AWAIT_REVIEWS |
0 |
Set to 1 to wake Claude when bot reviews and CI settle after a push (see below) |
CLAUDE_WATCHDOG_FIX_RECOMMENDATIONS |
0 |
Set to 1 to have Claude apply the analysis’s recommendations after presenting it |
CLAUDE_WATCHDOG_SELF_CHECK |
0 |
Set to 1 to have Claude self-check each turn that changed something (see below) |
Advanced overrides
These settings are available only as environment variables and are not exposed in
the plugin configuration prompt:
| Variable |
Default |
Purpose |
CLAUDE_WATCHDOG_LOG |
~/.claude/logs/claude-watchdog.log |
Debug log path |
CLAUDE_WATCHDOG_LOG_MAX_LINES |
1000 |
Log rotation threshold (lines). Past it, the log is trimmed to 80% of the threshold. The Stop hook logs one summary line per event (session id, stop reason, stop_hook_active, background-task count); use CLAUDE_WATCHDOG_DUMP_EVENTS for full payloads |
CLAUDE_WATCHDOG_MAX_BYTES |
51200 |
Condensed transcript size cap (weighted: 20% user messages, 80% recent context) |
CLAUDE_WATCHDOG_TMP |
${CLAUDE_PLUGIN_DATA} |
Plugin-owned data root. Per-session files live in a sessions/ subdirectory underneath |
CLAUDE_WATCHDOG_ANALYSES_DIR |
~/.claude/logs/claude-watchdog-analyses |
Directory for persisted analysis results (capped at 20) |
CLAUDE_WATCHDOG_VERBOSE |
0 |
Set to 1 to include a truncation notice in condensed transcripts |
CLAUDE_WATCHDOG_DUMP_EVENTS |
unset |
Set to a directory to write the raw stdin of every hook invocation there, one file each, for capturing test fixtures. Off unless set, and never changes hook behaviour. See tests/fixtures/CAPTURE.md |
CLAUDE_WATCHDOG_LOCAL_SESSION_STORAGE |
1 |
Set to 0 to store session files in the global plugin data path instead of the project directory |
CLAUDE_WATCHDOG_HOLD_TTL_SECONDS |
240 |
How long the input hold blocks prompts before auto-releasing |
CLAUDE_WATCHDOG_LEGACY_HOOK |
false |
Set to true to emit the analyzer instruction on stderr with exit code 2 instead of the default stdout JSON decision: block |
CLAUDE_WATCHDOG_AWAIT_TIMEOUT_SECONDS |
1200 |
Give up waiting for checks to settle after this long (must stay under the hook’s 1800 s timeout) |
CLAUDE_WATCHDOG_AWAIT_POLL_SECONDS |
30 |
Interval between GitHub status polls |
CLAUDE_WATCHDOG_AWAIT_GRACE_SECONDS |
90 |
Minimum wait after a push before an all-green head counts as settled, so a bot that registers its status late is not missed |
CLAUDE_WATCHDOG_AWAIT_QUIET_SECONDS |
90 |
Minimum time since the last status or check changed before the head counts as settled, since bots flip their status to done before posting the review |
CLAUDE_WATCHDOG_AWAIT_MAX_ROUNDS |
3 |
Maximum wake-ups per PR per session, so a review/fix loop cannot run forever |
CLAUDE_WATCHDOG_GH |
gh |
gh binary the await hook calls |
With Hold input while analysis runs enabled, the Stop hook drops a per-session
pending- sentinel when it triggers the analyzer, and a UserPromptSubmit hook
blocks any prompt submitted while that sentinel is fresh — useful when the analyzer
gets backgrounded and the input box reopens before the analysis lands. A blocked
prompt is not lost: press up-arrow to restore it. The hold releases when:
- the analyzer finishes (the SubagentStop hook clears the sentinel), or
- you resubmit — the first block marks the hold; the next prompt overrides it
(this is also the escape hatch after cancelling the analyzer with Esc/Ctrl-C,
since Claude Code fires no hook on cancellation), or
- the TTL expires (
CLAUDE_WATCHDOG_HOLD_TTL_SECONDS, default 240 s).
Two caveats: a prompt that lands in the instant between the analyzer finishing and
Claude presenting the analysis is not held (the analysis is already persisted to
the analyses directory at that point), and the hook adds one Node cold-start
(~30–80 ms) to every prompt submission while the plugin is installed.
Self-checking each turn
The post-mortem runs at most every 10 minutes and only on larger slices. With
Self-check each turn enabled, the Stop hook also asks Claude, at the end of
every turn that edited a file or ran a mutating shell command, whether it would
ship the work as it is. If not, Claude names the problem in one sentence and
fixes it. The bar is set high: a gap in what was asked, a bug, or a design
choice it already regrets, not style or polish. A clean turn ends with
✓ Self-check: no major issues.
- It runs at most once per prompt. The continuation it causes is not checked
again, and the post-mortem gates then apply as usual, so with both on the
analyzer reviews the fixed turn.
- A turn runs from the last prompt you typed. Task notifications and subagent
hand-backs do not start a new one, and edits made by subagents the turn
dispatched count as its own. With Skip while background tasks run off, a
background subagent’s edits that land after the check are not reviewed.
- Only the current turn counts: a read-only reply after an editing turn is not
checked.
- It ignores the cooldown and minimum tool calls. It honours
disabled,
.claude-watchdog-skip, and the background-task gate.
- It is self-review, run by the same model in the same context. It costs one
extra turn, but shares the model’s blind spots; the analyzer stays the
independent check.
Enforcing pinned subagent models
An agent definition can pin a model in its frontmatter:
---
name: cheap-reviewer
model: haiku
---
Claude Code honours that pin only when the dispatch leaves model unset - passing
model explicitly overrides it, which is easy to do by accident and silently moves
the work onto a different (usually pricier) model.
With Enforce pinned subagent models enabled, a PreToolUse hook blocks any
Task/Agent dispatch that names a pinned agent without an explicit model, and
tells Claude which model to re-dispatch with. A deliberate override still passes -
the point is that it has to be deliberate, not implicit.
The agent definition is looked up as <agent>.md or <agent>/<agent>.md, first
under $CLAUDE_PROJECT_DIR/.claude/agents, then ~/.claude/agents. An agent with
no model: key, or model: inherit, is never enforced. Every other path allows the
dispatch, so a malformed definition or an unreadable directory cannot wedge Claude.
Claude Code >= 2.1.178 also accepts Agent(model:opus) as a native permission rule.
That denies (or allows) a specific model value and complements this hook, which
blocks a missing model - the native syntax cannot express “parameter absent”. The
hook is a no-op when CLAUDE_CODE_SUBAGENT_MODEL_FORCE is set, since that overrides
every per-spawn and definition model anyway.
Waking on bot reviews and CI
Review bots such as CodeRabbit post a few minutes after a push, usually after Claude
has ended its turn, so the feedback sits there until you ask Claude to look.
With Wake on bot reviews and CI enabled, a background (asyncRewake) PostToolUse
hook runs after any Bash call containing git push or gh pr create. If the branch
has an open PR, it polls the head commit’s statuses and check runs until none is
pending and none has changed for 90 s, then collects failed checks plus bot (user.type == "Bot") reviews and
comments posted since the push. If there is anything, it wakes Claude with that list
and asks it to address the feedback under your usual instructions. Otherwise it exits
silently.
- A newer push supersedes a running watcher. A second trigger for a head that is being watched, or whose round already finished, is a no-op.
- A
.claude-watchdog-skip file in the session’s working directory disables it, as it does the post-mortem.
- It stops after 3 wake-ups per PR per session (
CLAUDE_WATCHDOG_AWAIT_MAX_ROUNDS).
- It needs an authenticated
gh CLI. Every failure (no PR, no gh, API errors) exits silently.
You can also create a .claude-watchdog-skip file to disable the hook for a project. The hook looks for it in the session’s working directory, so put it at the directory you start Claude Code from:
touch .claude-watchdog-skip # add to .gitignore if needed
On-demand analysis
Don’t want to wait for Claude to stop? Run /analyze-session any time during a conversation and Claude will analyze the session-so-far using the same criteria.
How it works
- Claude Code fires the
Stop hook when a turn ends.
session-analysis.mjs receives the event JSON (session id, transcript path, cwd, stop reason) on stdin.
condense.mjs filters the JSONL transcript down to user text, assistant text, tool calls, and tool results, keeps the last ~50 KB, and writes it to ${CLAUDE_PLUGIN_DATA}/sessions/condensed-<session-id>.txt (owner-only permissions; falls back to ~/.claude/tmp/claude-watchdog/sessions/ when not running as an installed plugin). When Store transcripts in project directory is enabled, files are written to <project>/.claude/tmp/claude-watchdog/sessions/ instead — this keeps them inside the project directory so Claude Code’s auto mode doesn’t prompt for Read permission. The project directory is the nearest ancestor of the session’s cwd holding .git or .claude, so a turn that ends with the shell inside a subdirectory still writes to one place per project. The per-session cursor and lock always stay in the global path, so a session that moves into a git worktree keeps its place. Files older than 2 hours are cleaned up automatically in both locations.
- It writes
{"decision": "block", "reason": ...} to stdout and exits 0. The reason instructs Claude to spawn the session-analyzer subagent pointed at that file. Setting CLAUDE_WATCHDOG_LEGACY_HOOK=true opts back into the old behaviour - the same instruction on stderr with exit code 2 - for hosts that do not honour the JSON decision.
- The subagent reads the condensed transcript, runs
git diff / git log in the working directory, and produces the structured review — all inside your current Claude Code session, using the model you’re already authenticated with.
- When the subagent finishes, Claude Code fires the
SubagentStop hook. persist-analysis.mjs reads the subagent’s final message from the event payload and writes it to ~/.claude/logs/claude-watchdog-analyses/ — so the subagent itself doesn’t have to call Write — then prints Analysis saved to: <path> on stdout so the save location shows up in the transcript.
What the condensed transcript looks like
One line per event, prefixed with its kind:
| Prefix |
Meaning |
USER: |
A prompt that started a turn |
USER (mid-turn): |
A message typed while Claude was still working (Claude Code queues it, then injects it into the running turn). Authoritative user input — the analyzer weighs it the same as USER: |
USER (mid-turn, origin=…): |
A queued prompt injected by automation (a cron, a hook) rather than by a person |
USER (edited file): |
A file the user edited by hand during the session |
ASSISTANT: / THINKING: |
Claude’s replies and reasoning |
TOOL_USE: / TOOL_RESULT: |
Tool calls and their results, truncated to 500 chars; a failed or refused call is labelled TOOL_RESULT[Name][ERROR]: so the failure is read before the body |
SYSTEM[…]: |
Everything else, including hook blocks and entry types the condenser doesn’t know about |
=== FINAL ASSISTANT MESSAGE (session ended here) === |
Claude’s concluding turn, appended after condensation so it is never truncated away — without it the transcript ends on tool results and the deliverable is invisible to the analyzer |
Pure session bookkeeping (window titles, PR links, mode pings, the last-prompt cache, queue enqueue/dequeue records) is dropped — it duplicates real message entries and otherwise crowds real content out of the byte budget. When the transcript busts the budget, user messages are kept ahead of tool traffic, and both ends of the user thread are kept (the opening goal and the most recent asks) rather than only the beginning.
No data leaves your machine except through Claude Code’s normal model calls.
Analysis results are saved to ~/.claude/logs/claude-watchdog-analyses/ (capped at 20 most recent sessions) so you can review past post-mortems.
Uninstall
/plugin uninstall claude-watchdog
/plugin marketplace remove Temikus/claude-plugins
/plugin uninstall clears ${CLAUDE_PLUGIN_DATA} automatically. Optionally delete the log, analyses, and any pre-plugin-era temp files:
rm -f ~/.claude/logs/claude-watchdog.log
rm -rf ~/.claude/logs/claude-watchdog-analyses
rm -rf ~/.claude/tmp/claude-watchdog # only exists on pre-0.4 installs or custom CLAUDE_WATCHDOG_TMP
Development
Requires jq and just in addition to Node.js:
just lint # validate JSON manifests and JS syntax
just test # run the whole suite
just check # lint + all tests
just test-perf # perf budgets (not part of `just test`)
just fixture-sanitise PATH # strip secrets from a captured fixture
See justfile for the full list, and
tests/fixtures/CAPTURE.md for how to capture a new
hook-event or transcript fixture.
License
Apache 2.0. See LICENSE.
评论