Claude Code wrote it, CodeRabbit reviewed it
Everything on this site is written by Claude Code and shipped by one person who does not read every line closely enough to catch a security bug. That is a real gap, and it is the awkward kind: the code looks fine, the page works, the tests pass. So I put a second AI in the way — CodeRabbit, which reviews pull requests — and gave it something real to read.
The change was a CSV export for the pricing calculator. The tool keeps everything in the browser, which is fine for playing with numbers and useless the moment someone wants to keep a record or send it to a client. So: one row per material line, then other costs, total cost, selling price, profit and margin. About sixty lines. My own checks passed first — the site audit script, and a headless browser run of both tools, 14 of 14.
Round 1: it said nothing
The first review came back with one line:
No actionable comments were generated in the recent review.
That is the default. CodeRabbit ships a review profile called CHILL, which is built not to nag.
For a team already drowning in review comments that is probably the right default. For someone who wants to know
what is wrong with code they cannot fully audit themselves, silence is the least useful answer available.
So I committed a .coderabbit.yaml asking for the strict profile:
reviews:
profile: assertive
Same commit, same diff, same reviewer. Only the setting changed.
Round 2: a real bug, found the hard way
The strict pass flagged one thing, and it was correct. CSV formula injection (CWE-1236): a
spreadsheet treats a cell beginning with =, +, - or @ as a
formula. Name a product =1+1, export it, and the number two shows up in the file the other person
opens. Pick a nastier formula and it stops being a curiosity. My csvCell() escaped quotes and
commas properly and did nothing whatsoever about this.
What I did not expect was how it checked. Folded into the comment was a shell command it had actually
run against the repository — a ripgrep over the file for every place a product name or unit
flows into the exporter — to see whether something upstream already sanitised the value before it reached
the CSV. It did not pattern-match on the words "CSV" and "user input" and fire. It went and looked.
The part where you cannot just click Accept
It also offered a committable patch, and applying that patch would have introduced a different bug.
The suggestion guarded the escaping with typeof value === "string", on the reasonable theory that
numbers need no protection. But in this exporter every number has already been through toFixed()
before it arrives, so a loss of -1.50 turns up as a string beginning with a minus sign. The
patch would have quoted it into text, and the profit column in the exported sheet would have quietly stopped
adding up. The reviewer knew the vulnerability class. It did not know what the values in this particular
function actually are by the time they get there.
So the fix I shipped skips any cell that is a plain number and quotes the rest.
Round 3: it caught the hole in my fix
My first version of that guard tested for a number with a hand-written pattern: an optional minus, some
digits, optionally a dot and more digits. The next review pointed straight at it: -1e-7 starts with
a minus, fails that test, and would be exported as text. That is reachable here, because raw material amounts
are not run through toFixed(), so a small enough amount really does stringify into exponent
notation.
It was right again, so I threw my regex away instead of patching it:
if(/^[=+\-@\t\r]/.test(text) && isNaN(Number(text))){
text = "'" + text;
}
Let the language decide what a number is. -1e-7, -.5 and -1.50 pass
through untouched; =1+1, @sum and +5x get quoted.
That version went back through the same strict review and came back with nothing to add.
What I actually learned
- The default setting hides the product. Had I tried it once, seen the cheerful nothing and moved on, my conclusion would have been "it finds nothing, so it does nothing." The finding was sitting there the whole time, behind one line of config.
- It is good at known classes of bug in unfamiliar code. CSV injection is exactly the sort of thing that is well documented, easy to skip while writing, and invisible to a passing test suite.
- It is weaker on the consequences of its own advice. Both times the finding was right and the patch needed a second pass — once because of what the values already were, once because of a case the pattern missed.
- Two AIs disagreeing beat either one alone. What shipped is not what Claude Code wrote, and it is not what CodeRabbit suggested. It is the third thing that came out of the argument.
Cost, honestly
Reviews on public repositories are free with no time limit, and this repository is public. Signing up put me on a 14-day trial of a paid plan without asking for a card; when that ends, a public repository carries on being reviewed. Private repositories are the part you pay for, and I have not paid for anything.
The wider number has not moved since the post about running this whole thing with subagents: still no revenue, on any of it. What changed today is that a tool I hand to other people no longer writes files that can execute when someone opens them.
Frequently asked questions
Is CodeRabbit free?
Reviews on public repositories are free with no time limit. Signing up starts a 14-day trial of a paid plan with no credit card, and when that ends a public repository keeps getting reviewed. Private repositories are the part you pay for.
Why did the first review find nothing?
The default review profile is called CHILL, and it is deliberately quiet. Adding a
.coderabbit.yaml with the assertive profile and asking for a full review produced a real finding on
exactly the same commit.
What is CSV injection?
A spreadsheet treats a cell that starts with =, +, - or @
as a formula. If an exported CSV contains user-entered text like =1+1, opening the file in Excel or
Google Sheets evaluates it instead of showing it. Prefixing the value with an apostrophe keeps it as text.
Can you just apply the suggested fixes?
Not blindly. The suggested patch here guarded on the value being a string, but every number in this exporter
already goes through toFixed(), so a profit of -1.50 arrives as a string and would have
been quoted into text, breaking arithmetic in the exported sheet.