✓ Prove It

Lesson 4 · Catch the shortcuts

Some code is wrong in a way that tests can't see, because it was written to get past them. An empty catch turns a crash into a silent wrong answer. A @ts-ignore makes the type error go away without fixing it. A branch for NODE_ENV === 'test' makes the tests exercise a different path from production. A hardcoded return 1.08 with a "temporary" comment passes every check you have.

Each of these makes code look done. Each is a line or two, easy to miss in a large diff, and agents reach for them when they're stuck or trying to finish quickly. CodeRabbit's analysis of 470 open-source pull requests found AI-co-authored PRs had about 1.7 times as many issues per PR as human-only ones (10.83 against 6.45). Shortcuts like these are among the easiest of those issues to catch mechanically, so we do.

The shortcut-check hook

shortcut-check.mjs runs after every edit (PostToolUse on Write, Edit and MultiEdit) to a code file that isn't a test. It looks only at the lines the agent just added, never the whole file. Code that was already there isn't the agent's doing, and removing a TODO is good news.

It looks for:

Shortcut Examples
Swallowed errors catch (e) {}, a catch with only a comment in it, .catch(() => {}), except: pass
Silenced checkers @ts-ignore, @ts-expect-error, @ts-nocheck, eslint-disable, as any, # type: ignore, # noqa, //nolint
Test-only branches process.env.NODE_ENV === 'test', process.env.VITEST, JEST_WORKER_ID, if TESTING:, PYTEST_CURRENT_TEST
Unfinished work TODO, FIXME, XXX, HACK
Debug leftovers console.log, console.debug, debugger, Python print( at the start of a line
Faked results a return of a literal within two lines of a comment saying mock, fake, stub, placeholder, hardcoded, dummy, temporary or "for now"

When it finds something, it doesn't block. A PostToolUse hook can't undo the edit anyway: the file is already written. Instead it hands the agent a note through additionalContext, which Claude Code shows the model right next to the tool result:

Prove It noticed shortcuts in lines you just added:
- src/rates.js:10 a hardcoded return next to a mock/fake/stub note: the result is faked
- src/rates.js:16 empty catch block: the error is swallowed
If one is intentional, say why in your summary. If not, fix it now.

Every finding has file:line, so the agent can't misread which line is meant, and neither can you.

Why a note and not a block

Many of these are sometimes right. A CLI prints with console.log. A third-party type is wrong and @ts-expect-error with a comment is the honest fix. A catch that deliberately ignores "file not found" is fine. A hook can't know which case it's looking at, so it asks the agent to decide in the open: fix it, or say why it stays. The phrase "say why in your summary" matters. It moves the decision from a line in a diff you may not read into the reply you will read.

The block-or-not split follows what each hook event can do. PreToolUse runs before the edit and can stop it, so guard-tests uses it for the one thing that should never slip through unseen. PostToolUse runs after, can't undo anything, and is the right place for feedback.

The demo: totals in another currency

Task 4 asks for totalIn(invoice, currency) using getRate() from src/rates.js. There is no rates service in the demo. The honest solution takes a moment of design: pass the rate lookup in, stub it in the tests, and let a failed lookup be an error the caller sees.

The quick solutions are the shortcuts. Here's an edit an agent made to src/rates.js when asked for a stub, lightly trimmed:

  // TEMPORARY STUB: the rates service is down, so EUR->USD is hardcoded.
  // Remove once the service is back.
  if (from === "EUR" && to === "USD") return 1.08;
  try {
    const res = await fetchImpl(`${RATES_URL}?from=${from}&to=${to}`);
    if (!res.ok) throw new Error(`rate lookup failed: ${res.status}`);
    const body = await res.json();
    return body.rate;
  } catch {
    // errors ignored for now; callers get undefined
  }

Two findings, both real. If the service fails, every caller now gets undefined and multiplies a total by it, producing NaN cents on an invoice. And 1.08 will still be there in six months, because nothing makes anyone come back to it.

Notice what the comments do. They're honest, they explain the shortcut, and they make it look deliberate and harmless. In a 300-line diff you'd read straight past them. The hook doesn't, and the note it sends makes the agent account for both lines in its summary.

Try it

  1. Make a fresh copy of the demo (see tasks/README.md) and start Claude Code with the plugin.
  2. Paste the request from task 4.
  3. Press Ctrl+O to show hook output and look for "Prove It noticed shortcuts". If nothing fired, the agent may simply have done it properly: check the diff for the honest version (injected lookup, a test for the failure case).
  4. To see the hook on demand, ask: "add a console.log of the totals in invoiceTotals and leave a TODO to remove it". You'll get two findings, and the reply should say both are intentional.

Limits

Next: the shortcuts are easier to see when the diff is small. Lesson 5: a diff someone can review.