A hands-on look at CodeRabbit on a real, absurdly large feature-removal PR.
Sponsored by ** CodeRabbit** — but this is a real PR, on a real codebase, with real findings. I’m going to show you exactly what the tool caught, what it got wrong, and whether the “AI reviews your PR” pitch holds up when the PR is 218 files and minus‑74,096 lines.
AI writes more code than ever. Reviewing it shouldn’t mean scrolling forty files in alphabetical order.
CodeRabbit Review reorganizes any pull request from a flat file list into a structured, layer-by-layer walkthrough - the logical reading order of the change, not the order your platform happens to sort it. Every range gets its own plain-language summary, with sequence diagrams, state machines, and ERDs generated inline wherever a visual earns its place.
Cohorts group related files and chunks so you review one idea at a time. Layers order them so foundational changes - data shapes, contracts - come before the code that depends on them. Code Peek lets you click any variable, function, class or type to see its definition and usages without leaving the tab, while Semantic Diff view cuts past formatting noise to show what actually changed.
Open it straight from the Review Change Stack button in the PR Walkthrough. Navigate cohorts and layers from the keyboard, comment against exact line ranges and submit native reviews, comments and approvals post back to GitHub or GitLab right where your team expects them.
In the early access, available for free to everyone.
From the team that pioneered AI code reviews. 2M reviews every week. 6M repos. 15K customers.The review interface built for the way modern PRs are actually written.
Review your next PR with CodeRabbit Review Today
The setup: killing a feature that outgrew the game #
HiddenWars is a browser-based hacking strategy game — Go + Echo on the backend, Svelte 5 on the frontend, PostgreSQL, Redis.
The Corruption War was a cooperative map/season meta-game that had accumulated ~74k lines of code and too many design docs. It was time for it to go.
218 files changed · +1,453 / −74,096 · 5 phases · one SQL migration
This is the worst-case input for an automated reviewer. Most of the diff is deletions, the codebase has a genuinely confusing naming collision (more on that in a minute), and the “interesting” changes are surgical edits buried inside shared files. If a tool can produce a useful review here, it can produce one anywhere.
I installed CodeRabbit on the repo, pointed it at the PR, set the profile to Chill (the balanced middle option — more on the three profiles later), and let it run.
What follows is a look at what it caught on the Go specifically, what it got wrong, and whether the “AI reviews your PR” pitch holds up when the PR is 218 files.
The hard part: a Go interface that mixed two systems #
The single gnarliest file was backend/internal/db/botnets.go. It defines a repository interface that he botnet subsystem hangs off:
type BotnetRepository interface {
ListPlayerNodes(ctx context.Context, playerID string, limit, offset int, filter NodeListFilter) ([]models.PlayerNode, int, error)
// ... healthy, degraded, corrupted node management ...
// Siphon-scoped locking: pins draining nodes to a specific session so
// concurrent siphons keep separate draining pools.
LockNodesForSiphon(ctx context.Context, playerID, slug string, count int, sessionID string) (int, error)
UnlockNodesBySiphonSession(ctx context.Context, playerID, sessionID string) error
// War-op-scoped locking: pins healthy/degraded nodes of slug to a hack op as
// 'war_committed' (ownership via war_op_id). Returns the locked node IDs.
LockNodesForWarOp(ctx context.Context, playerID, slug string, qty int, opID string) ([]string, error)
// UnlockWarOpNodes reverts the player's surviving war_committed nodes for opID ...
UnlockWarOpNodes(ctx context.Context, playerID, opID string) error
// LiveOpComposition returns the current surviving committed composition per
// player for an op: counts only nodes still state='war_committed'.
LiveOpComposition(ctx context.Context, opID string) (map[string]models.NodesComposition, error)
LiveOpDegraded(ctx context.Context, opID string) (map[string]map[string]int, error)
LiveOpAbsorbers(ctx context.Context, opID string) (map[string]map[string][]models.PlayerNode, error)
// DegradeWarOpNodesTracked damages up to maxCount of the player's war_committed
// nodes of slug pinned to opID, using the hack damage model ...
DegradeWarOpNodesTracked(ctx context.Context, playerID, opID, slug string, maxCount int, damage, corruptThreshold float64) (int, []models.NodeDamageEntry, error)
// ... raid, bounty, siphon, mining, signature counts ...
}
The trap is the word corruption. This interface, and this whole file, contains two unrelated mechanics:
-
The Corruption War — the
WarOp/LiveOpmethods above, thewar_committednode state. This is what I’m deleting. -
Node corruption —
corruptednodes, corruption spread, thecorruption_immunitybuff. A separate botnet maintenance mechanic that stays.
A reviewer — human or AI — that confuses those two will either delete the wrong code or drown you in false “you’re removing the wrong thing!” comments. I was specifically watching for how CodeRabbit handled it.
The Go that needed surgery, not deletion #
The war methods came out cleanly — eight method signatures deleted from the interface, eight implementations deleted below. The subtle work was the other Go in the file: methods that stay, but whose raw SQL references war states inline. Take Se verNodes:
func (b *Botnets) SeverNodes(ctx context.Context, playerID string, nodeIDs []string) (int, error) {
if len(nodeIDs) == 0 {
return 0, nil
}
conn := b.Acquire()
defer conn.Release()
rows, err := conn.Query(ctx, `
DELETE FROM player_nodes
WHERE id = ANY($1)
AND botnet_id = (SELECT id FROM botnets WHERE player_id = $2)
AND state NOT IN ('locked', 'committed', 'raid_committed', 'war_committed', 'region_recon',
'boss_committed', 'hashing', 'draining', 'recompiling')
RETURNING id
`, nodeIDs, playerID)
// ...
}
war_committed
and region_recon
in that NOT IN
list are war coupling — they have to come out. But ’locked’
, ’committed’, ’raid_committed’
, ’corrupted’
(further up) all stay, because those are raid/bounty/node-corruption mechanics. This is raw SQL as a Go string literal: your compiler will not help you. There is no type checker that catches a stray ’war_committed’
left in a query. This is exactly where an AI reviewer earns its keep — if it can read the SQL.
CodeRabbit did. It never flagged the legitimate states, and it correctly identified every war-coupled string across the file. The naming collision it had to reason through: *this corrupted
is the keep-mechanic, that war_committed
is the delete-mechanic.* It inferred the distinction from context. That’s the moment I stopped thinking of it as a linter.
The same pattern, in a 60-line reaper query #
Where it got genuinely impressive was ReleaseOrphanedLockedNodes
— a background reaper that’s one big CTE with a UNION
per node state. Each arm reaps nodes stuck in a transient state (locked, committed, hashing, draining) when their owning op/session has vanished. The war removal meant: delete the war_committed
arm entirely, but leave the raid/bounty/siphon/decryption arms alone. The surviving query looks like this (trimmed for length):
func (b *Botnets) ReleaseOrphanedLockedNodes(ctx context.Context) (int64, error) {
conn := b.Acquire()
defer conn.Release()
tag, err := conn.Exec(ctx, `
WITH orphaned AS (
-- committed nodes whose raid is completed
SELECT pn.id FROM player_nodes pn
JOIN botnets bot ON bot.id = pn.botnet_id
WHERE pn.state IN ('committed', 'raid_committed')
AND pn.locked_at IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM raid_registrations rr
JOIN raids r ON r.id = rr.raid_id
WHERE rr.player_id = bot.player_id AND r.completed_at IS NULL
)
UNION
-- locked nodes with no live bounty hunt ...
UNION
-- hashing nodes with no active decryption event ...
UNION
-- draining nodes whose owning session ended
SELECT pn.id FROM player_nodes pn
JOIN botnets bot ON bot.id = pn.botnet_id
WHERE pn.state = 'draining'
AND pn.locked_at IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM siphon_sessions ss
WHERE ss.id = pn.siphon_session_id AND ss.status = 'active'
)
)
UPDATE player_nodes
SET state = CASE WHEN health < 70 THEN 'degraded' ELSE 'healthy' END,
locked_at = NULL
WHERE id IN (SELECT id FROM orphaned)
`)
// ...
}
There used to be a fifth arm here — a war_committed
reaper that found nodes whose war op had resolved. Deleting it is trivial. The hard part is being confident the remaining four arms don’t have a hidden war dependency. CodeRabbit read the query and was satisfied that they didn’t. A human reviewer would have to do the same read; CodeRabbit did it in the time it took me to refill my coffee.
The SQL-in-Go the AI actually flagged #
CodeRabbit did have an opinion about this file — a Major
on the draining arm of that reaper. The bot noticed that when the reaper resets an orphaned draining node, it clears state
and locked_at
but leaves siphon_session_id
dangling:
UPDATE player_nodes
SET state = CASE WHEN health < 70 THEN 'degraded' ELSE 'healthy' END,
locked_at = NULL
WHERE id IN (SELECT id FROM orphaned)
Its suggested fix:
UPDATE player_nodes
SET state = CASE WHEN health < 70 THEN 'degraded' ELSE 'healthy' END,
locked_at = NULL,
siphon_session_id = NULL
WHERE id IN (SELECT id FROM orphaned)
This is a real observation about state hygiene in a reaper query. I ultimately didn’t take it — and here’s the honest part of an honest review.
The feature that sold me: “Prompt for AI Agents” #
Every CodeRabbit comment ships with a collapsible 🤖 Prompt for AI Agents — a machine-readable instruction formatted for a coding agent. The real one for the reaper finding:
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/internal/db/botnets.go` around lines 2783 - 2796, Update the reaper’s
player_nodes UPDATE for orphaned draining nodes to clear siphon_session_id along
with resetting state and locked_at. Ensure reused nodes no longer retain stale
siphon ownership that could be acted on by delayed UnlockNodesBySiphonSession
cleanup.
This is the workflow shift. You don’t read the comment and paste it into your agent — you hand it the prompt and it knows the file, the line range, the intent, and the validation contract. I ran my whole review-fix loop this way: collect the prompts → feed them to my coding agent → have it assess each against the current Go and fix only the still-valid ones. The review stops being a thing you read and becomes a thing you execute.
The three review profiles #
CodeRabbit offers three, set in .coderabbit.yaml
:
Quiet: Only the most important findings, inline | Mature codebases, noisy PRs, low reviewer bandwidth |
Chill: (what I used) | Balanced — Majors and Minors, nitpicks separated | General purpose; the sane default |
Assertive: More feedback, may feel like nitpicking | Greenfield, junior-heavy teams, strict bars |
I ran Chill and it felt right for a removal PR. Assertive on 218 files would have fatigued me; Quiet would be my pick for a hotfix where I only want “will this break prod?”
So, is it worth it? #
Here’s the honest accounting for Go shops specifically. Go gives you a strong type system, go vet, staticcheck, and good test tooling — so a lot of what AI review catches in dynamically-typed languages, Go already catches at compile time. What’s left for an AI reviewer in Go is exactly what bit mhere:
raw SQL embedded as string literals, interface/method consistency across a big refactor, and cross-file invariants the compiler can’see.
botnets.go is 3,500 lines of pgx queries against a state machine living in a Postgres CHECK constraint — no Go tool in my toolchain reasons about the SQL inside those backticks. CodeRabbit does.
The mental model that worked for me: CodeRabbit is a very fast, very thorough junior reviewer who never gets tired but never says “I don’t know, what do you think?” It always has an opinion, and some will be wrong in the particular way a competent-but-context-light reviewer’s are. Your job shifts from finding issues to adjudicating them — strictly faster work.
On a 74,000-line removal PR, it caught two real things I’d missed and cost me maybe ten minutes adjudicating the few misses. For a Go team too small for thorough human review — it’s worth a serious look.
Review your next PR with CodeRabbit Review Today