An independent Gemini reviewer that audits Claude Code's claimed progress against the real working tree, feeds corrections straight back into Claude's context, and stops everything to ask you when a decision is yours to make.
By Kayne Middleton. MIT licensed.
The hooks are global, so once installed it is available in every project, and it
stays dormant in all of them until you arm one. Config, key and logs live in a
data directory outside the code (~/.claude/overseer/ by default), so updating
or reinstalling never touches your settings.
task_plan.md and progress.md are written by the agent being audited. An
overseer that only reads those grades Claude's homework with Claude's own
answer key: a hallucinated [x] reads as success. So every review is sent
git diff <baseline> plus git status, and the model is told the diff is the
only evidence. A checklist item marked done with nothing in the diff is the
main thing it exists to catch.
The baseline is the last commit older than OVERSEER_LOOKBACK_HOURS (12 by
default), so the diff spans work you committed mid-task as well as the
uncommitted tree. Without that, committing mid-task would empty the diff and
the overseer would falsely accuse Claude of fabricating everything.
Your global and project CLAUDE.md go in the prompt too (plus
CLAUDE.local.md if present), so it also enforces scope lock, the comment
policy, the banned tooling, and plain ASCII.
Rules it cannot see are rules it cannot enforce, so the global file is never
truncated and the project file gets whatever is left of rules_chars (80k by
default). A very large project CLAUDE.md can still be clipped; --config
will not tell you that, but a [... truncated marker in the prompt means raise
the budget.
- Claude Code
- Python 3.9 or newer on
PATH, andgit pip install google-genai(the only dependency; everything else is stdlib)- A Gemini API key from https://aistudio.google.com/apikey
- The planning-with-files plugin. This is not optional: every review is an
audit of
task_plan.md,findings.mdandprogress.mdagainst the diff, and theStophook refuses to let substantive work finish with no plan behind it. Without those files the overseer has nothing to audit.
Add the planning-with-files marketplace first. The overseer declares planning-with-files as a plugin dependency, but Claude Code will only fetch a dependency from a marketplace you have already configured, and it refuses to pull one from a different marketplace than the plugin declaring it unless that marketplace is allowlisted. Both sides are set up here, so the add is the one step left to you:
/plugin marketplace add OthmanAdi/planning-with-files
Then the overseer, which brings planning-with-files in with it:
/plugin marketplace add anytech/overseer
/plugin install overseer
Installing and enabling the overseer installs and enables planning-with-files at
the same scope. You do not install it separately, and you cannot disable it
while the overseer is enabled: Claude Code refuses and hands you a chained
command that disables both. If you uninstall it anyway, /reload-plugins and
the background plugin update put it back, because its marketplace is configured.
Skip that first marketplace add and the dependency is left unresolved. The overseer installs but stays disabled with a dependency error until you add the marketplace, at which point Claude Code resolves it on its own.
The dependency carries no version constraint, so it tracks whatever
planning-with-files publishes. The overseer reads task_plan.md,
findings.md and progress.md under .planning/<date>-<slug>/; if upstream
ever moves those, pin the version in .claude-plugin/plugin.json.
The install dialog asks for one thing, your Gemini API key, and stores it in
your OS keychain rather than in settings.json. Everything else ships on a
working default and is changed later through the config file or the environment,
so there is nothing to decide at install time. Hooks are wired for you.
Confirm the key actually landed, then arm the repos you want audited. Every hook exits immediately in an unarmed repo, so nothing happens until you arm one:
/overseer:status
/overseer:arm
/overseer:disarm stands a repo down again, /overseer:review runs one review
on demand, and /overseer:watch gives you the command for the live viewer. That
is the whole command surface, and none of it needs you to know where the plugin
put its files.
If you would rather not use the plugin system:
git clone https://github.com/anytech/overseer ~/.claude/overseer
pip install -r ~/.claude/overseer/requirements.txt
python ~/.claude/overseer/monitor_plan.py --set-key
python ~/.claude/overseer/monitor_plan.py --install-hooks
--set-key prompts for the key and writes it to <data dir>/.key with
owner-only permissions. --install-hooks backs up settings.json first, is
idempotent, records the absolute path of the interpreter that ran it, and adds
any hook that an older install is missing, so re-run it after every upgrade.
The clone can live anywhere; the hooks point at wherever you put it.
Everything below is written as <overseer>, which is the full path to
monitor_plan.py. A clone install is wherever you cloned it. A plugin install
is inside the plugin cache, under a directory named for the plugin version:
%USERPROFILE%\.claude\plugins\cache\anytech\overseer\<version>\monitor_plan.py
Do not memorise that. /overseer:status prints the live path on its first line,
and /overseer:watch hands you the viewer command with the path already filled
in, so a version bump cannot leave you holding a stale one.
python <overseer> --config
That prints the script path, the data directory, every setting, where each value
came from, whether a key resolved, and whether planning-with-files is installed.
api key resolved: NO means every review fails open and nothing will ever be
blocked. This is the one failure worth checking for, because the overseer stays
quiet either way. planning-with-files: NOT INSTALLED prints the exact commands
to fix it, and every session in an armed repo opens by telling the agent to put
those commands to you.
Pick a model with --list-models to see what your key can actually use. The
default is gemini-3.5-flash-lite, which is cheap and good enough for diff
auditing; set model_stop to something stronger if you want the end-of-turn
review sharper than the mid-turn ones.
Then try a paid review by hand from inside an armed project:
python <overseer> --once
| What | Where |
|---|---|
| Code | the clone, or the plugin's install directory |
| Config, key, notification state, escalation log | the data directory |
| Data directory | $OVERSEER_HOME, else ~/.claude/overseer/ |
| Per-repo review log and review state | .planning/<plan>/ in the audited repo, gitignored automatically |
| The armed marker | .overseer-on at the audited repo's git root, gitignored automatically |
Keeping the data directory outside the code is what makes a plugin update safe: plugins install into a versioned directory that is replaced on upgrade, so a key stored next to the code would not survive one.
Reviews, blocking, plan repair and debriefs work on Windows, macOS and Linux.
Desktop notifications are Windows only. On macOS and Linux notify() returns
immediately and everything else behaves normally, so you get the blocks and the
corrections but no tray balloon.
The plugin hooks run through sh, which means Git Bash on Windows. If you do not
have it, install from the clone instead: --install-hooks writes the absolute
path of your Python interpreter and needs no shell at all.
The hooks are installed globally but exit immediately unless the repo holds an
.overseer-on marker file. From inside a project, either /overseer:arm and
/overseer:disarm, or directly:
python <overseer> --enable # arm this repo
python <overseer> --disable # stand it down
--enable writes the marker at the git root, prints where it put it, and adds
it to that repo's .gitignore. Both matter: the marker must never be
committed, and an untracked file would otherwise turn up in the git status
handed to Gemini as evidence in every single review.
The search for the marker walks up from the working directory and stops at the git root, so a marker left in a parent folder full of checkouts cannot silently arm every project underneath it. In an unarmed repo the hook exits before it reads a plan file, runs git, or touches the network.
Manual runs (--once, --watch) ignore the marker, so you can review anything
on demand without arming it.
In an armed repo:
| Hook | Fires | Effect |
|---|---|---|
SessionStart |
Every new session | Mandates the planning-with-files skill, and says how to install it if it is missing |
PreToolUse |
Claude is about to ask you a question | Denies the question when the plan already answers it |
Stop |
Claude says it is finished | Blocks with the review; Claude must fix and stop again |
PostToolUse |
Any file is written | Rule-checks what was just written; reviews the plan when a plan file changes |
In every repo, armed or not:
| Hook | Fires | Effect |
|---|---|---|
Notification |
Claude asks you something or wants permission | Tray balloon, and a line in the watcher |
Notification is deliberately not gated: being told Claude is sat waiting on
you costs nothing and matters everywhere. Silence it with OVERSEER_NOTIFY=0.
Claude Code's idle notification is a wall-clock timer on the input box, so it fires while subagents are still running and tells you Claude is waiting when it is not. Before passing an idle alert on, the overseer scans the transcript for tool calls that have no result yet, and stays quiet if anything is still in flight. Permission prompts are never suppressed this way; only the idle one.
For its first 76 reviews this tool did not enforce the rules at all. The log
proves it: one finding of kind rule_violation, for a rule nobody had written
("phase boundary markers"), and not a single citation of the bans on sed,
grep, git commit, plain-ASCII text, comment policy or fallbacks. Two
structural faults caused it.
The PostToolUse hook was registered for every Edit|Write|MultiEdit, then
threw away anything that was not task_plan.md, progress.md or
findings.md. So the overseer had never once looked at a line of code at the
moment it was written, and all 19 of those reviews came back on_track,
because bookkeeping was all they ever saw. Meanwhile the Stop review asked
about rules as item 3 of a 10-point list, behind roughly 150k characters of
plan, diff and rulebook. The same model, asked the same question on its own,
gets it right; buried, it never fired once.
The fix is one dedicated call that is asked nothing else.
An earlier version of this tool tried to do the job with hand-written gates
instead: pattern matchers for banned commands, a character sweep for em dashes
and smart quotes, regexes for comment runs and docblocks. They are all gone. A
gate that greps for sed has to be taught that parsed and based are not
sed, then taught about git add versus git commit, then taught that a Herd
binary path is not the banned herd, and it still only ever catches the rules
somebody remembered to hand-code. The model reads the rulebook and applies all
of it, including the rules written after the code shipped. Deleting the gates
lost nothing and removed the part that needed maintaining.
Your rulebook is split and audited section by section. gather_rules()
collects the global CLAUDE.md, the project one and CLAUDE.local.md.
rulebook_chunks() splits that at markdown headings and packs it to roughly
2500 characters a chunk. Each chunk gets its own concurrent call, eight at a
time, carrying only that slice of the rules and only the new code. Nothing is
buried behind 150k characters of plan and diff, and nothing is asked to weigh
ten questions at once.
The whole audit is bounded by audit_budget (60 seconds). Sections that fail or
run out of time are counted, and when a run finishes with unfinished sections and
no findings the log says so in as many words: a clean result there is not
evidence of clean code.
Every finding has to prove itself twice, or it is dropped. A finding must carry a verbatim quote of the rule it invokes and a verbatim quote of the line that breaks it. The rule quote must be at least 20 characters and appear in the chunk that was sent; the code quote must appear in the code that was sent. Both comparisons are made on flattened text, so whitespace and formatting differences do not matter, but an invented rule or a paraphrased line does not survive.
That verification is the whole reason the audit is trustworthy. The one bogus
finding in this tool's early history was a rule nobody had written; that class of
finding can no longer reach you, because there is no section of your CLAUDE.md
it can be quoted from.
Findings are then deduped by offending line and capped at 12 per audit.
It runs twice, on additions only. Existing code is never audited:
- On each
Edit,WriteorMultiEdit, against what that edit adds, once the addition is over 120 characters. Identical additions are skipped by digest, and a session gets at mostmax_blocksedit-time blocks. A confirmed breach blocks immediately, naming each rule and each offending line, and tells Claude to fix that file before carrying on. - At
Stop, against every line the whole task added, cached on a hash of the rules plus the additions so the same diff is never paid for twice. Here a confirmed breach forces the verdict todeviatedat confidence 100 even if the plan review saidon_track, and jumps the queue in the corrective prompt.
So a breach the per-edit pass missed, or one that only makes sense across several files, is still caught before the turn ends.
Rules were pulled out of the main Stop prompt when this call was added. That
review now judges the plan against the diff and is told explicitly not to report
rule violations, because this call owns them.
A rule you disagree with can be excused per repo. In the watcher, x
excuses every rule the selected entry cites, clicking one finding excuses just
that rule, and u undoes the last excusal. Each one writes the rule's hash to
.overseer-exempt at the repo root, and later audits in that repo drop findings
that quote it. Nothing else is ever silenced.
Turn the edit-time pass off with rule_check_edits: 0. The Stop sweep always
runs in an armed repo.
Claude asking "which phase next?" when the plan lists the phases in order is an
interruption you should never have to field. A PreToolUse hook on
AskUserQuestion catches the question before the dialog appears, checks it
against the plan with a small focused call, and denies it when the plan settles
it, handing Claude the answer as the denial reason:
Do not interrupt for this. Your own plan already answers it:
Build Phase 3.
From the plan: - [ ] Phase 3. Decisions stream.
It cannot fill in the dialog, only stop it appearing. The check is deliberately biased towards letting questions through: a genuine product, business or risk call the plan never made passes straight to you, because interrupting is cheaper than guessing on your behalf. Denials are capped at two per session, so a wrong deflection can never leave Claude unable to ask anything.
Claude Code's own Notification hook only fires after roughly a minute of
idling, so a finished turn can sit there in silence while you look elsewhere.
The Stop hook therefore raises its own alert the instant Claude hands control
back, carrying the verdict and summary. It fires in every repo, armed or not,
and it never fires when the overseer blocked, because a block means Claude is
still working and needs nothing from you. OVERSEER_NOTIFY_DONE=0 turns it off
without silencing escalations.
Two mechanisms, because a prompt alone is a suggestion:
SessionStartinjects the mandate into Claude's context: use planning-with-files in slug mode, keeptask_plan.md,findings.mdandprogress.mdunder.planning/<date>-<slug>/, and never tick an item ahead of the work, because the overseer reads the diff.Stoprefuses to let Claude finish substantive work with notask_plan.mdbehind it. That check is deterministic and runs before the API call, so it costs nothing and cannot be argued with. Substantive means two or more files touched, a diff over 1200 characters, or new untracked files, so genuine one-liners and question-answering turns are never held up.
PostToolUse also exits immediately unless the edited file is task_plan.md,
progress.md or findings.md, so it costs nothing on normal edits.
When Gemini judges that a decision is yours, it sets needs_human and the hook
blocks with the question, instructing Claude to put it to you verbatim via
AskUserQuestion rather than guessing. You also get a tray balloon, and every
escalation is appended to escalations.jsonl.
The bar in the system prompt is deliberately high: a product or business call, a requirement with two defensible readings, an irreversible action, anything touching live production data or money. Never for something Claude could settle by reading the code.
Escalations ignore the block cap. Corrections do not.
"Say the word and I'll do it", "want me to do that next", "shall I continue".
Every one of those is the agent stopping to ask permission for work it should
have done, and the rules forbid it. The Stop hook reads what Claude actually
said from the session transcript, quotes the offending line back, and tells it to
either do the work or ask a specific question rather than end the turn on an
offer.
Detection is two passes over the same closing message. Fifteen anchored patterns
run over the last 800 characters with no API call and need no judgement, so the
obvious phrasings cannot be talked around. A model call then classifies whatever
the patterns missed, and the two results are merged. Both are subject to the
per-session block cap, so with max_blocks at 2 the third offer in a session
goes through unblocked.
Every hanging action is classified by who is allowed to do it, because the answer differs completely:
| Who | Example | What happens |
|---|---|---|
| the agent | "the tests still need writing" | blocked, told to do it now |
| the owner decides | "which phase next, 4, 5 or 6?" | blocked, told to ask it properly with AskUserQuestion and wait |
| the owner acts | "needs a bump and build" | stop allowed, logged YOUR MOVE, balloon lists the actions |
The classifier is told the agent is forbidden from committing, pushing, deploying, releasing, running live APIs and touching production, so anything of that shape is yours however casually it was mentioned. A message can carry all three at once, and then the agent does its own share first, asks the question, and leaves your deploys alone.
That last row matters: a deploy the agent may not perform is not an incomplete task and must not be pushed back at it. It is a handoff, and the point is that it arrives as a list rather than buried in the last paragraph of a summary.
Two layers catch it, and neither is the main review:
- A pattern match on the obvious phrasings, free and instant.
- Failing that, a separate tiny call carrying only the closing message and one question: did it hand any action back? Blocked-on-a-missing-credential is explicitly excluded, since that is a legitimate stop.
The second layer exists because of a measured failure, not a hunch. The full
review was given the closing message and an explicit rule about it, and still
returned on_track with 100 confidence, twice, on two different turns. The
same model, asked the same question in isolation, gets it right every time and
quotes the exact sentence. The signal was drowning in 150k characters of diff
and rules, not missing. Model tier was never the problem, and a bigger model
would not have fixed it: a focused question would.
Correct work is not finished work. If a review is otherwise clean but the plan
still has unticked items that are in scope, the overseer sets continue_work
with the specific next_step and the Stop hook sends Claude straight back to
it instead of letting the turn end. The badge for this is KEEP GOING.
It is told not to use it when the plan is genuinely complete, when the remaining items were explicitly deferred or belong to a later phase, or when the next step needs a human decision first, which is an escalation instead.
Pushes have their own budget, OVERSEER_MAX_CONTINUES (default 3), separate
from the correction budget, so a disagreement about "finished" cannot loop.
What is outstanding is computed, not interpreted. The prompt carries a
section built in code from the plan's - [ ] items, stating either the exact
list or that nothing is outstanding, and the model is told it outranks the
prose. That exists because a model reading a 100KB plan reliably mistakes
retained history for live work. It also means you never need a "summarise
what's left" rule in CLAUDE.md: a written summary would be another self-report,
and the checkboxes are already the machine-readable answer.
Outstanding work is unticked - [ ] items plus any Phase N heading that
carries no tasks at all. A plan that describes Phase 4 in prose and never
breaks it down still has Phase 4 left to do, and counting boxes alone would
call that finished. A heading marked closed, complete, done, superseded,
retired or shipped is skipped, and a parent phase whose sub-phases hold the
tasks is covered by them.
A push must be grounded in the plan's own unticked boxes. A model reading a
100KB plan cannot reliably tell a superseded scope table from the live one, and
it will happily report retired history as outstanding work. So when the model
says the plan is incomplete, that claim is checked against - [ ] items in
task_plan.md. If the plan uses checkboxes and none are open, the push is
dropped and the model's claim is kept as ungrounded_open_items for the
record. If boxes are open, they replace the model's prose, so next_step
always points at something the plan actually tracks. A plan written without
checkboxes falls back to trusting the model.
Grounding works in both directions: it drops a push the plan does not support,
and it creates one the model missed. The model calling a plan finished does not
make it finished any more than its prose makes it unfinished. It only ever
adjusts a push, though. A deviated or false_completion verdict still
blocks on its own merits, and an escalation is untouched.
A phase with no tasks under it, or a plan with no checkboxes at all, is not
work to push at Claude. It is a defect in the plan that makes every later
review guesswork. That gets its own outcome, badged FIX PLAN: Claude is told
to break the phase into - [ ] items and nothing else, tick none of them,
and stop again. Two minutes of editing, after which the plan can state what is
left and normal grounding takes over.
It is self-clearing by construction. The trigger is the absence of tasks, so performing the repair removes the trigger, and the nag cannot recur.
Claude Code records exact token usage per assistant message, so context
pressure is measured, not guessed. Past context_debrief_percent (60 by
default) the Stop hook takes over:
- First time, it blocks and sends Claude back to write the handover into
progress.md,findings.mdandtask_plan.md-- on disk, where it survives compaction -- explicitly not as a chat summary, since chat is the thing that gets compacted. - Next stop, with the handover written, it lets the turn end and tells you the session is safe to compact.
- It re-arms only after another 50k tokens, so a long session gets a fresh debrief at 85%, 92% and so on rather than nagging every turn.
The overseer cannot run /compact. That is verified rather than assumed: the
installed CLI binary contains no triggerCompact, requestCompact,
compactRequest or runSlashCommand literal, so no hook output can ask for
one. It does contain autoCompact in abundance, so the session will compact
itself eventually whatever anyone does.
So the overseer covers the part that actually loses work, from both ends. It
pushes Claude to write the handover while the session is still alive, and it
hooks PreCompact to write one itself the moment compaction begins, auto or
manual. That second file, handover.md beside the plan, is authored by the
overseer from the transcript and the plan's own checkbox state, so it exists
whether or not Claude cooperated:
# Handover, written at compaction on 2026-08-11T22:20:53+00:00
- trigger: auto
- context at compaction: 918,885 tokens (92%)
- plan: 44 of 46 checkboxes ticked
## Outstanding, from the plan itself
- [ ] 4.1 central feed ingest
- phase with no tasks: Phase 6. Scale the control plane
## What the agent last said, newest first
...
Set context_window to match the model you run: the default 1,000,000 suits a
1M-context Opus, and on a 200k model it would never fire until you lower it.
max_blocks, max_continues and max_repairs are loop guards, so they count
consecutive blocks about the same unresolved situation. Anything changing,
whether the plan moved on or a different defect appeared, resets the budget:
| Stop | Situation | Result |
|---|---|---|
| 1, 2 | Phase 4 has no tasks | blocked |
| 3, 4 | Phase 4 still has no tasks | let through, it has been said twice |
| 5, 6 | now Phase 7 has no tasks | blocked, fresh situation |
| 8 | plan resolved | clean stop, counters reset |
- Fails open. Missing key, network error, quota exhaustion, unparseable response: it exits 0 silently and logs. It can never wedge Claude.
- Loop guard. Corrections block at most
OVERSEER_MAX_BLOCKStimes per session (default 2), and it never re-fires on astop_hook_activepayload. - Deduped before the call, not after. The fingerprint of the plan plus the diff is computed first, and an unchanged repeat reuses the cached verdict rather than paying for an identical answer. A cached block still blocks.
- Only blocks on a correction when confidence is at or above
OVERSEER_CONFIDENCE(default 70). - Every review is appended to
.overseer-reviews.jsonlin the plan directory. If it starts producing junk, read that before trusting it.
Everything lives in config.json in the data directory ($OVERSEER_HOME, else
~/.claude/overseer/). Claude Code's settings.json is only ever touched to
register the hooks themselves, and nothing else needs to go in it.
python <overseer> --init-config # write it
python <overseer> --config # what is in effect
config.example.json in the repo documents every key inline; copy it over
config.json if you would rather read than guess.
--config prints every setting with where it came from, which matters because
an environment variable silently outranks everything else. Precedence is
environment variable, then plugin option, then config.json, then the built-in
default. The install dialog sets one plugin option, the API key, so every other
setting is yours to change here or in the environment. A malformed value falls
back to the default rather than crashing a hook.
Keys may be written either bare (model_stop) or prefixed
(OVERSEER_MODEL_STOP). A typical two-model setup:
{
"model": "gemini-3.5-flash-lite",
"model_stop": "gemini-3.1-pro-preview",
"timeout_ms": 90000
}The API key is looked for in this order: GEMINI_API_KEY, GOOGLE_API_KEY, the
plugin install dialog, api_key in this file, then .key in the data directory.
--config names the one that won, so you never have to guess which copy is live.
Prefer the install dialog or --set-key. The dialog value goes to your OS
keychain, --set-key writes .key owner-only, and neither leaves the key in a
file you might later paste, commit or copy somewhere public.
Every one of these has a config.json equivalent without the OVERSEER_
prefix, in lower case. Use whichever suits; the variable wins.
| Variable | Default | Purpose |
|---|---|---|
GEMINI_API_KEY |
- | Key, or GOOGLE_API_KEY, or .key |
OVERSEER_MODEL |
gemini-3.5-flash-lite |
Reviewing model |
OVERSEER_MODEL_STOP |
same as above | Model for the blocking stop review |
OVERSEER_CONFIDENCE |
70 |
Minimum confidence to block on a correction |
OVERSEER_MAX_BLOCKS |
2 |
Correction blocks per session |
OVERSEER_MAX_CONTINUES |
3 |
Consecutive "keep going" pushes |
OVERSEER_MAX_REPAIRS |
2 |
Consecutive "fix the plan" blocks |
OVERSEER_LOOKBACK_HOURS |
12 |
How far back the evidence diff reaches |
OVERSEER_RULES_CHARS |
80000 |
Ceiling for global plus project CLAUDE.md |
OVERSEER_RULE_CHECK_EDITS |
1 |
Rule audit on each substantive edit, 0 leaves only the one at Stop |
OVERSEER_AUDIT_WORKERS |
8 |
Rulebook sections audited concurrently |
OVERSEER_AUDIT_BUDGET |
60 |
Seconds the whole rule audit may take |
OVERSEER_CONTEXT_WINDOW |
1000000 |
Context size of the model Claude runs |
OVERSEER_CONTEXT_DEBRIEF_PERCENT |
60 |
When to force a written handover |
OVERSEER_TIMEOUT_MS |
45000 |
Gemini request timeout |
OVERSEER_NOTIFY |
1 |
Tray balloons at all, 0 silences every one |
OVERSEER_NOTIFY_DONE |
1 |
Alert the moment a turn ends and it is your go |
OVERSEER_NOTIFY_REPEAT |
600 |
Seconds before the same message may repeat |
OVERSEER_ASCII |
unset | Force the plain ASCII watch UI |
OVERSEER_COLOR |
unset | 1 forces colour even when stdout is piped |
NO_COLOR |
unset | Standard opt-out, disables all colour |
Each review returns both a corrective_prompt written at Claude, and an
owner_briefing written at you: plain English on what it actually did and
whether to trust it. Same single API call, so watching costs nothing extra.
Every field lands flat in .overseer-reviews.jsonl (verdict, confidence, issues,
needs_human, human_question, and the rest) so a log line stands on its own.
-
--watchis a two-pane terminal app over those logs. It never calls Gemini itself, so it cannot double up on cost, and it surfaces the fail-open errors hook mode otherwise swallows. Scoped to the current repo;--allshows every project.--intervalsets the log poll in seconds, default 2, independent of the ~12fps repaint.The left pane is the index, one row per review with its time and badge:
CLEAR,NITS,KEEP GOING,DEVIATED,INCOMPLETE,FALSE CLAIM,YOUR CALL,WAITING(Claude wants you) orFAILED(the overseer broke). The right pane is the selected review in full: briefing, verdict, confidence, every issue, the corrective prompt and any escalation. The bottom bar tallies reviews, flagged and escalations, and shows how long it has been quiet.
| Key | Does |
|---|---|
| up / down, k / j | Move through the index |
| pgup / pgdn | Scroll the detail pane |
| home | Jump to the oldest review |
| end, f | Jump to newest and resume following |
| x | Excuse every rule this review cites, in this repo |
| u | Undo the last excusal |
| q, esc | Quit |
Clicking a row in the left pane selects it, and clicking a single rule finding in the right pane excuses just that one rule.
It follows the newest review until you move the selection, then holds still
so a live arrival cannot yank the thing you are reading out from under you.
f resumes following. An escalation rings the terminal bell.
It runs on the alternate screen buffer, the same one vim and less use, so the terminal's own scrollback does not apply and your shell comes back untouched when you quit. The app keeps its own history instead: the whole log for the current scope is loaded at startup and every new review is appended, so the index is the full record and nothing scrolls away. The log file remains the durable copy.
It degrades to ASCII on a console that cannot render UTF-8, and drops colour
entirely when piped or when NO_COLOR is set. Keyboard control is Windows
only; elsewhere it simply follows along.
--onceruns one live review and prints it.--cwd <path>targets a project other than the current directory.
Nothing at all in an unarmed repo. In an armed one: a review per plan-file
write, a rule check per substantive edit, and per stop a review plus a rules
audit. The stop review runs from about 50k characters on a small task to 150k
on a heavy one, dominated by the diff and the closing message. Raise
OVERSEER_LOOKBACK_HOURS and it grows.
Budget for the rule audit as several calls, not one. The rulebook is split into
roughly 2500-character chunks and each chunk is audited separately, so the call
count is your rulebook size over 2500: a 22k-character CLAUDE.md is 12 calls
per audit, and the rules_chars ceiling of 80k would be about 32. Each call is
small, carrying one slice of the rules plus only the new code, but the count is
what to watch if you have a large rulebook. audit_workers caps how many run at
once and audit_budget caps the whole thing in seconds.
The per-edit audit is the one that scales with how busy Claude is. It sees only
the text the edit introduced, skips anything under 120 characters, and dedupes on
a content hash so a retried write is not paid for twice. Set
rule_check_edits: 0 to drop it and keep only the audit at Stop, which is
itself cached on a hash of the rules plus the additions.
Dropping rules out of the main stop prompt paid for a good part of this: that prompt is now roughly 35k characters lighter.
Model choice matters less than call structure. A lite-tier model has been observed missing an explicit instruction it was given in full, and the fix was never a bigger model. It was asking one question at a time, on one slice of the rules at a time, and throwing away any answer that cannot quote the rule and the offending line back verbatim.
Split the models rather than paying pro rates on everything. Stop is the
review that blocks, and there is one per turn, so it is the one worth spending
on. Mid-task PostToolUse reviews can stay cheap: set model and model_stop
in config.json.
Run --list-models first; ids come and go, and a listed model can still 404.
The stop hook is installed with a 120 second timeout for this reason. If a pro
review runs longer than OVERSEER_TIMEOUT_MS (45s) it fails open, meaning no
review at all, so raise that too if you see FAILED rows in the viewer.