Skip to content
Back to skills

Loop Watchdog

ASecurity

Lane 1 of the loop. Runs three drift checks against this repo — model IDs seen in real Claude Code transcripts that are missing from the committed allowlist, PRICING rates against Anthropic's published rates, and log-schema fields the parser depends on. Decides what counts as a finding, files it as a `loop:watchdog` issue, and hands it to `loop-pipeline`. Use when the user says "run the watchdog", "check for drift", "are the prices still right", "any new models", or on the daily scheduled run.

  • 7 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 23, 2026
datago

Works with

  • claude code

Security analysis

A100/100

Scanned September 23, 2026

npx -y skills add simantaturja/cccost-dashboard --skill loop-watchdog --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Loop Watchdog?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Loop Watchdog
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/simantaturja-loop-watchdog/badge)](https://www.skillsdirectory.com/skills/simantaturja-loop-watchdog)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: loop-watchdog
description: >-
  Lane 1 of the loop. Runs three drift checks against this repo — model IDs seen
  in real Claude Code transcripts that are missing from the committed allowlist,
  PRICING rates against Anthropic's published rates, and log-schema fields the
  parser depends on. Decides what counts as a finding, files it as a
  `loop:watchdog` issue, and hands it to `loop-pipeline`. Use when the user says
  "run the watchdog", "check for drift", "are the prices still right", "any new
  models", or on the daily scheduled run.
---

# Loop watchdog

You look for one thing: **the dashboard reporting a confident wrong number.**

`getRates` (`lib/core.js:38-43`) resolves a model ID by substring, first match
wins. That fails two ways:

- **Loud drift** — an ID matches nothing, is priced at $0, and
  `unknownModelMessages` increments (`lib/core.js:344`). Visible if someone looks.
- **Silent drift** — a new ID substring-matches an *older* entry and is billed at
  that older model's rates. A future `claude-opus-6` matches `'opus'` and prices
  at Opus-5. No counter moves, no test fails, the number is just wrong.

Silent drift is why this lane exists. Commit `ce7b49d` was this bug found by
hand.

## Hard stops

- Never merge a PR. Never commit or push to `master`.
- Never run `npm publish` or `vsce publish`, and never invoke `/release`.
- Never apply `loop:go` or `loop:build`. Those are the human's gates.
- **Never write a pricing rate you did not fetch and cannot cite.** A wrong rate
  is worse than a missing one — a missing one increments a counter someone sees,
  a wrong one bills silently. If you cannot fetch the published rate, file the
  issue and stop there.
- Append to `.loop/log.md` on **every** run, including the ones that find
  nothing. The quiet entries are what stop a non-finding being re-reported
  tomorrow.

## Check 1 — new model IDs

The silent-drift check. This is the important one.

Collect every distinct model ID appearing as `message.model` in
`~/.claude/projects/**/*.jsonl`. Compare against the `priced` and
`intentionallyUnpriced` arrays in `test/fixtures/known-models.json`.

Any ID in the transcripts and in neither array is a finding. For each, report:

- the exact ID
- how many messages carry it
- **which `PRICING` entry it currently matches**, by running the same
  first-match-wins substring walk `getRates` does — or `null` if it matches none

That last field is the whole point. `null` is loud drift, annoying but visible.
A *match* is silent drift: state plainly which model's rates it is currently
being billed at and what that means for the numbers already on the dashboard.

## Check 2 — rate drift

Compare every rate in the `PRICING` table (`lib/core.js:4-19`) against
Anthropic's currently published rates.

**Fetch them at run time. Never answer this check from model memory** — your
knowledge has a cutoff and pricing changes without one. If the fetch fails, say
the check did not run. A skipped check reported as skipped is fine; a check
answered from memory is how the wrong number gets committed.

Report any rate that differs, with both values and the source URL.

## Check 3 — log-schema drift

The parser depends on these fields. Confirm each still appears in transcripts
from the last few days:

`usage.*` (the token counts), `speed`, `message.model`, `tool_use_id`, `cwd`

A field that has vanished from recent transcripts is a finding — the parser is
silently reading nothing where it used to read data.

## Dedup — before you file anything

Two sources, both required:

1. `.loop/log.md` — what previous runs already decided, including what they
   decided *not* to file and why. A finding recorded here as "no action" stays
   no action unless something changed; say what changed if you re-open it.
2. `gh issue list --search "label:loop:watchdog,loop:audit" --state all --json number,title`
   — what is already filed. The comma inside `--search` is an OR; separate
   `--label` flags would be an AND and match nothing.

A finding present in either is not new. Append a one-line note saying you saw it
again and move on.

## Acting on a finding

For each genuinely new finding:

1. `gh issue create --label loop:watchdog` with the finding, its evidence, and —
   for check 1 — the current mis-match and its cost consequence.

2. Then decide whether you can also *fix* it:

   - **Yes, when the fix is machine-checkable and you have the facts.** Adding an
     observed ID to `test/fixtures/known-models.json`, or adding a `PRICING` row
     whose rates you fetched in check 2 and can cite. Hand it to `loop-pipeline`
     with the issue number and acceptance criteria — for a pricing change the
     criteria must include a test pinning the new rates.
   - **No — file and stop.** When the correct rates are unknown, when the fix
     needs a judgment about ordering in the substring table, or when check 3
     found a schema change whose right response is a parser design decision.
     Say in the issue why you stopped rather than building.

   `PRICING` is ordered and first match wins. A new entry that is a substring of
   an existing one, or vice versa, changes how *other* IDs resolve. If a row's
   position is not obvious, that is a human's call — file it.

## Logging — every run

Append to `.loop/log.md` in the format `loop-pipeline` defines:

```
## YYYY-MM-DD loop-watchdog

- **Finding:** one line
- **Action:** filed #N / PR #N / no action
- **Why:** one line — especially when the action was "no action", since that is
  what stops this finding being re-reported tomorrow
```

Record all three checks each run, including the ones that found nothing and the
ones that could not run. "Check 2 skipped, rate fetch failed" is a real entry
and a useful one.

Where the log gets committed:

- **Filed something** — `loop-pipeline` commits it on the `loop/<slug>` branch.
- **Quiet run** — commit it yourself on a `loop/log-<date>` branch and push that.
  Never on `master`, so the human gate holds even for a no-op run.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.