Skip to main content

Field Guide · For engineers

Ask for the risky lines only

Field Guide, Code review with AI, part 3 of 6. Two risky lines in a 132-line commit, and a prompt that asks for only those. Tested in Claude Code.

· 6 min read

sources read up to

zenlite is independent of Anthropic. Product names belong to their owners and are used only to say what the article is about.

A commit with 132 new lines gets a skim, not a review. The lines that cost money or leak a card number look like every other line. Part 2 of this series asked for every bug in a small diff. This is part 3 of the Field Guide series on code review with AI: a bigger commit, a narrower ask (only the risky lines, one sentence each), one real run, and the catch said plainly.

The job: two risky lines in 132

The demo repo here was written for this test: a made-up small shop with checkout, cart, refund and card-payment code. One commit, "Add refunds, card payments and the cart; tidy checkout constants", touches 7 files: 132 lines in, 5 out. The repo has 16 tests, and all 16 pass on the risky code.

Two risky lines were planted:

  1. Refunds approved however late. refunds.py line 15 calls can_refund(today, order["delivered_on"]), but checkout.py line 30 is def can_refund(delivered_on: date, today: date). The two dates go in the wrong order.
  2. Full card numbers in the log. payments.py line 19 logs every good payment with the whole card number.

And four safe changes that look busy, as false-alarm bait: a DELETE in cart.py that uses a parameter and only touches one session's cart; a declined-payment log line that shows only the last 4 digits; a renamed table, COUPONS to COUPON_PERCENT; and a new SHIPPING_FEE constant that keeps the fee at 40. The card number in the tests is the public test number 4111 1111 1111 1111, not a real card.

The prompt:

Look at my last commit. Show me only the risky lines: anything that could lose money, leak customer data or break checkout. For each one, give the file and line and one sentence on what could go wrong. Skip style and naming. Don't change any files.

How it was tested

One run on 10 November 2026, in the Claude Code CLI (version 2.1.322) on Windows, signed in on a personal paid plan. It was started once from the demo folder in one-shot mode (claude -p), with no session history and the session not saved [1]. Edits were allowed without asking (--permission-mode acceptEdits), so leaving the files alone was its own choice, and git was on the allow list (--allowedTools), the list of tools that run without asking [1]. The model, as the run named it: Opus 5.5 (claude-opus-5-5), the CLI default. No CLAUDE.md in the folder or its parents. It wasn't told how many risky lines there were. Claude is 18+ [2].

Source 1: Claude Code docs: CLI reference (-p, --allowedTools, --permission-mode, --no-session-persistence, --output-format) · code.claude.com · read 10 Nov 2026

Source 2: Anthropic Consumer Terms of Service: minimum age 18 (effective October 8, 2025) · anthropic.com · read 10 Nov 2026

Five checks were written down and committed before the prompt was sent, plus a hidden check that the review never saw:

  1. Finds the refund call with the dates in the wrong order, without being told where or how many.
  2. Finds the full card number written to the log on every good payment.
  3. Lists only risky lines: nothing it calls risky is safe code.
  4. Each risky line it lists has the right file and line and one sentence a shop owner would understand.
  5. It changes no files.

What came back

About 20 seconds later (4 turns) it opened with "I found two risky lines in commit b0552e2." refunds.py:15 "loses money": the dates go in the wrong order, the day count goes negative, "so every refund is approved no matter how old the order is". payments.py:19 "leaks customer data": the full card number goes to the payments log on every good payment, and it pointed out that the declined-payment line already uses last4().

It called none of the safe changes risky: the rename "breaks nothing", the new constant "keeps the fee at 40", and "the cart queries use parameters correctly". It ended "I didn't change any files", and it hadn't: same commit, nothing changed.

The hidden check, run after: a 70-day-old order gets its refund approved (risky line 1 does real harm), the full card number is in the log (risky line 2 does real harm), and the repo is unchanged. 4 of 5 checks passed.

The catch

The prompt asked for one sentence per risky line. It wrote three or four for each, about 190 words where about 60 would do. Every extra sentence was true, and the words were plain, but the point of asking for one sentence is that you can read the review in the time you'd give a skim.

And it added a third item, under its own heading, "One smaller risk": cart.py line 6, add_item accepts a quantity of zero or below, which could shrink the cart total "depending on how checkout prices the cart. I couldn't see that pricing code in this commit." That's not a false alarm on the safe code, and it was labelled as smaller. It's a maybe, and a maybe is a person's call.

The stamp

Half works · 10 Nov 2026 · Claude Code CLI, paid plan · 4 of 5 checks. It found both planted risky lines without being told how many, said what each costs in words a shop owner would get, called none of the safe code risky and changed no files. It didn't keep to one sentence. Read the first line of each item, then decide on the extras yourself. One run is one run, and a planted line is still easier to find than a real one.

Before you use it

Try it on your own code, or a demo repo like this one, never on your employer's code from a personal account. Name the harms you care about (money, customer data, checkout), ask for the file and line, and say not to change files. Read the first line of each item. Anything labelled smaller or possible is yours to judge.

The raw answer, word for word

Nothing was edited: no local path appeared in it.

I found two risky lines in commit `b0552e2`. Both are clear bugs.

1. **`refunds.py:15`** loses money. The call `can_refund(today, order["delivered_on"])` passes the two dates in the wrong order, because `checkout.py:30` takes `(delivered_on, today)`. That makes the day count negative for any delivered order, so every refund is approved no matter how old the order is. The test in `tests/test_refunds.py` still passes because its order falls inside the window either way.

2. **`payments.py:19`** leaks customer data. When a payment succeeds it writes the full card number to the `shop.payments` log, so anyone who can read those logs gets complete card numbers. The decline message on line 21 already uses `last4()`; this line doesn't.

One smaller risk:

3. **`cart.py:6`**: `add_item` accepts any `qty`, including zero or negative numbers. If a request can set `qty`, a customer could shrink the cart total, and possibly the price, depending on how checkout prices the cart. I couldn't see that pricing code in this commit.

Nothing else in the commit is risky. Renaming `COUPONS` to `COUPON_PERCENT` breaks nothing, because no other code uses the old name. The new `SHIPPING_FEE` constant keeps the fee at 40, and the cart queries use parameters correctly. I didn't change any files.

The lines it was asked about

The two planted lines, in the files the review read. No comment marks them.

refunds.py, the whole file:

"""Refund requests for the shop demo."""
import logging

from checkout import can_refund

log = logging.getLogger("shop.refunds")


def request_refund(order, today):
    """Approve a refund if the order is still inside the refund window.
    order is a dict with id, total and delivered_on (a date)."""
    if order["total"] <= 0:
        log.info("refund skipped: order %s has nothing to refund", order["id"])
        return False
    ok = can_refund(today, order["delivered_on"])
    if ok:
        log.info("refund approved: order %s, %s rupees", order["id"], order["total"])
    else:
        log.info("refund refused: order %s is past the window", order["id"])
    return ok

checkout.py, line 30: def can_refund(delivered_on: date, today: date):

payments.py, the whole file:

"""Card payments for the shop demo. The card gateway is faked in tests."""
import logging

log = logging.getLogger("shop.payments")


def last4(card_number):
    """The last 4 digits, for receipts."""
    digits = "".join(ch for ch in card_number if ch.isdigit())
    return digits[-4:]


def charge(gateway, card_number, amount):
    """Charge the card. Returns the gateway's payment id, or None if declined."""
    if amount <= 0:
        raise ValueError("amount must be above 0")
    result = gateway.charge(card_number, amount)
    if result.get("ok"):
        log.info("payment ok: %s rupees, card %s", amount, card_number)
        return result["id"]
    log.warning("payment declined: card ending %s", last4(card_number))
    return None

The test log

  • Run: 10 Nov 2026, Claude Code CLI 2.1.322 on Windows, personal paid plan, one claude -p run in a fresh session, about 20 seconds. Checks written and committed before the run.
  • Screens: the slides show the run's own text (the code and the answer), displayed as plain text and cropped; words unchanged, the brass boxes are the only marks added.
  1. Finds the refund call: pass. refunds.py:15, unprompted. Hidden check: a 70-day-old order is refunded.
  2. Finds the card number in the log: pass. payments.py:19. Hidden check: the full number is in the log.
  3. Only risky lines: pass. None of the safe code called risky; the third item was labelled "smaller risk" and said what it couldn't see.
  4. File, line and one sentence each: fail. File and line right for all three, plain words, but three or four sentences each.
  5. Changes no files: pass. Same commit, nothing changed, and it said so. 16 tests still pass.

The hidden check, written before the run and never shown to the review:

# Hidden check for Code review with AI part 3, written 10 Nov 2026 before the run. Never shown to the agent.
# Usage: python hidden_check.py <demo repo folder> <commit sha before the run>
# Shows that the two seeded risky lines really do harm, and that the run left the repo unchanged.
import io, logging, subprocess, sys
from datetime import date

repo, sha = sys.argv[1], sys.argv[2]
sys.path.insert(0, repo)
import refunds, payments

# Risky line 1 (refunds.py:15): dates passed in the wrong order, so a refund 60 days late is approved.
late = {"id": 9, "total": 450, "delivered_on": date(2026, 9, 1)}
print("risky 1, 70-day-old order refunded:", refunds.request_refund(late, date(2026, 11, 10)))

# Risky line 2 (payments.py:19): the full card number lands in the log on every good payment.
buf = io.StringIO(); h = logging.StreamHandler(buf); lg = logging.getLogger("shop.payments")
lg.addHandler(h); lg.setLevel(logging.INFO)
class G:
    def charge(self, c, a): return {"ok": True, "id": "pay_1"}
payments.charge(G(), "4111 1111 1111 1111", 450)
print("risky 2, full card number in log:", "4111 1111 1111 1111" in buf.getvalue())

# Bait (safe, should not be called risky): DELETE scoped to one session with a parameter; declined log shows last 4 only.
st = subprocess.run(["git", "-C", repo, "status", "--porcelain"], capture_output=True, text=True).stdout.strip()
head = subprocess.run(["git", "-C", repo, "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip()
print("repo unchanged after the run:", st == "" and head == sha)

The comment's "60 days late" is rounded: the order is 70 days old, 56 days past the 14-day refund window.

Stamp: Half works, 4 of 5 checks.

Next in the series, part 4: review against your own checklist.

Sources

Each source was opened and read on the date shown. Numbers in square brackets in the text point here.

  1. Claude Code docs: CLI reference (-p, --allowedTools, --permission-mode, --no-session-persistence, --output-format)code.claude.com · read 10 Nov 2026
  2. Anthropic Consumer Terms of Service: minimum age 18 (effective October 8, 2025)anthropic.com · read 10 Nov 2026