Code Review skill for Claude Code
Findings ranked by severity, minus the nitpicks. Gives Claude a two-pass review method for teammates' diffs: first design and correctness traced with concrete adversarial inputs, then a sweep for auth gaps, injection, races, unsafe migrations, N+1 queries, and weak tests. Findings are verified before posting, labeled blocker through nit, carry a file and line plus a fix, and end with a verdict that states what was not reviewed.
- name
code-review - size204 lines · ≈2,150 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Reviews other people's diffs and pull requests for correctness, security, performance, and maintainability, ranks findings by severity, and writes specific, actionable comments with suggested fixes while skipping nitpicks a linter should catch. Works from a local branch, a git range, or a GitHub PR via gh. Use when the user asks to review a PR, look over a diff or branch, check code before merge, or give feedback on a teammate's change. Try asking
- Review PR #482 before I approve it
- Look over the diff on this branch and tell me what would break in production
- Give feedback on my teammate's migration and billing changes
Use it when
- A teammate's PR or branch needs a reviewer
- You want a second pass on risky code before merge
- The change touches auth, payments, migrations, or concurrency
- You need review comments phrased so the author can act without a meeting
Skip it when
- You want a deep OWASP-style security audit of a whole codebase — use owasp-review
- You are writing your own PR description — use pull-request-writer
How to check the result
Each blocker and major finding cites a file and line, names the triggering input, and was confirmed by running code or reading callers; the summary lists unreviewed files.
The common mistake it prevents
Burying one real concurrency or authorization bug under dozens of style nitpicks that a formatter should have handled.
A good review finds the defects that would hurt in production, explains each one so the author can fix it without a meeting, and stays quiet about taste. Ten precise findings ranked by severity beat fifty comments the author has to triage.
Ground rules
- Understand the intent before judging the code: read the PR description and linked issue first. A correct implementation of the wrong thing is a blocker.
- Read the whole diff, then the surrounding code it touches. Bugs hide in what the diff calls and what calls the diff.
- Every finding states: where, what goes wrong, under which input or condition, and a concrete fix. “This seems off” is not a finding.
- Verify suspicions cheaply when possible (run the test, grep for callers, check the type) before asserting. Mark unverified concerns as questions.
- Do not comment on formatting, import order, or naming nits that a formatter or linter could enforce. Suggest the lint rule once instead.
- Acknowledge good decisions briefly when they are non-obvious; it calibrates trust.
Workflow
- [ ] 1. Load context: description, issue, diff stats, CI status
- [ ] 2. Map the change: which files carry the logic, which are noise
- [ ] 3. Pass 1 — design and correctness of the core change
- [ ] 4. Pass 2 — checklist sweep (security, errors, data, perf, tests)
- [ ] 5. Verify suspicions (run, grep, reason with concrete inputs)
- [ ] 6. Rank findings by severity; cut nitpicks
- [ ] 7. Write comments and a summary verdict
1. Load context
gh pr view 123 # description, author, linked issues
gh pr diff 123 --name-only
gh pr checks 123 # do not review red CI line by line; ask for green first
gh pr checkout 123 # local copy to run and grep
# without gh:
git fetch origin pull/123/head:pr-123 && git switch pr-123
git diff --stat origin/main...HEAD
2. Map the change
Sort files into: core logic, tests, wiring/config, generated/lockfiles. Read core logic
first. For moved code use git diff --color-moved=zebra --color-moved-ws=allow-indentation-change origin/main...HEAD
so you review the real edits, not the relocation.
3. Pass 1 — design and correctness
Ask, for the core change:
- Does it solve the stated problem completely, including the edge cases in the issue?
- Trace two concrete inputs through the code: one typical, one adversarial (empty, null, huge, unicode, concurrent, duplicate, out-of-order).
- What happens on failure of each external call: timeout, error, partial success?
- Is the change in the right layer, or does it leak concerns (HTTP details in domain code, SQL in a view)?
- Does it duplicate something that already exists?
grep -rnfor similar helpers.
4. Pass 2 — checklist sweep
Quick checklist; for the full version with language-specific traps read references/review-checklist.md when the diff touches auth, money, concurrency, migrations, or public APIs.
- Security: untrusted input reaches SQL/shell/HTML/paths/URLs unescaped; authz check on every new endpoint and object access (IDOR); secrets in code or logs; new dependencies are legitimate and pinned.
- Errors: swallowed exceptions; errors logged and rethrown twice; missing cleanup
(
finally,defer, context managers); retries on non-idempotent operations. - Data: migrations safe on large tables; nullable vs. default; transactions span the right operations; time zones and money types.
- Concurrency: shared mutable state; check-then-act races; missing awaits; unbounded parallelism.
- Performance: queries inside loops (N+1); unbounded result sets without pagination; O(n^2) on user-sized input; work on the hot path that could be cached or deferred.
- API and compatibility: breaking changes to public functions, HTTP responses, events, config keys, or DB columns that other services read.
- Tests: new behavior has tests that would fail without the change; tests assert outcomes, not mocks; edge cases from pass 1 are covered.
- Operability: logs and metrics for new failure modes; feature flag or kill switch for risky paths.
5. Verify suspicions
gh pr checkout 123
npm test -- path/to/affected.test.ts # does the claimed coverage exist?
grep -rn "functionName(" src/ # all callers of a changed signature
git log -L :functionName:src/file.ts # history of a function before judging it
If you can write a 5-line failing test for a suspected bug, include it in the comment.
6. Rank by severity
| Label | Meaning | Merge? |
|---|---|---|
| blocker | Bug, data loss, security hole, broken contract, missing core test | Must fix before merge |
| major | Likely bug in edge case, perf problem at expected scale, risky design that will be costly to change later | Fix before merge unless author justifies |
| minor | Maintainability issue, unclear naming that misleads, missing test for secondary path | Fix now or ticket |
| nit | Taste; optional | Never blocks; prefix with nit: and keep to a handful |
| question | You do not understand something; may or may not be a problem | Needs an answer |
Cut: anything a formatter fixes, personal style preferences, rewrites of working code into “how I would have done it”, and speculative future-proofing.
7. Write comments
Comment shape: severity label, the problem with the concrete triggering condition, the consequence, the suggested fix.
**blocker:** `applyCredit()` reads the balance and writes it back in two queries
without a lock. Two concurrent refunds for the same account both read 100 and write
150 instead of 200. Wrap in a transaction with `SELECT ... FOR UPDATE`, or use an atomic
`UPDATE accounts SET balance = balance + $1`.
**question:** This catches `Exception` and returns an empty list. Is an empty list a
valid answer when the upstream is down? Callers in `report.ts` will render "no
invoices" instead of an error.
Use GitHub suggestion blocks for small, certain fixes:
```suggestion
if (items.length === 0) return [];
```
Phrase about the code, not the person (“this function” not “you”). Ask rather than command when you are not sure.
Post via CLI if requested:
gh pr review 123 --comment --body-file review.md
gh pr review 123 --request-changes --body-file review.md # only with blockers
gh pr review 123 --approve --body "LGTM. Two nits inline, non-blocking."
Summary verdict template
## Review: <PR title>
**Verdict:** Request changes — 1 blocker, 2 major, 3 minor.
**Summary:** Adds annual plans with proration. Core math is correct and well tested;
the blocker is a race in credit application under concurrent refunds.
### Blockers
1. `src/billing/credit.ts:48` — lost update on concurrent refunds (details inline).
### Major
1. `src/billing/plan.ts:112` — leap-year proration divides by 365 (details inline).
2. Migration `0042_add_interval` rewrites `subscriptions` (8M rows) under an
ACCESS EXCLUSIVE lock; split into add-nullable + backfill.
### Minor
- Missing test for downgrade from annual to monthly mid-cycle.
### Verified
- Ran `npm test` on the branch: 412 passed.
- Confirmed all callers of `calculateTotal()` updated for the new signature.
### Not reviewed
- `generated/` client and `pnpm-lock.yaml` (generated).
Always say what you did not review, so the author does not assume coverage.
Gotchas
- Reviewing only the diff misses deleted safeguards: check what was removed as carefully as what was added.
- Tests in the PR that mirror the implementation (mocking everything the code calls) prove nothing; check that at least one test would fail on a plausible bug.
- Renames plus edits in one commit hide logic changes; ask the author to split, or use
--color-moved. - Large PRs get rubber-stamped. If the diff exceeds ~800 hand-written lines, say so and review the riskiest part deeply rather than everything shallowly.
- Approving with “LGTM, minor comments” while a blocker exists in an unread file is the worst outcome. State coverage honestly.
Verification
- Every blocker and major has a file:line, a triggering condition, and a fix.
- Each claimed bug was verified by running code, reading callers, or reasoning through
a concrete input; unverified ones are labeled
question. - Nits are at most a handful and labeled; none block.
- The summary lists what was verified and what was not reviewed.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/review-checklist.md
# Full review checklist
Read this when the diff touches auth, money, concurrency, migrations, public APIs, or
when the quick checklist in SKILL.md flags an area. Skip sections that do not apply.
## Contents
- Authentication and authorization
- Input handling and injection
- Secrets and data exposure
- Error handling and resilience
- Concurrency and async
- Data, persistence, and migrations
- Money, time, and numbers
- Performance
- API and contract compatibility
- Tests
- Frontend specifics
- Language traps
## Authentication and authorization
- Every new route, RPC, GraphQL resolver, or job handler checks authentication.
- Object-level authorization: loading `/invoices/:id` verifies the invoice belongs to
the caller's tenant/user, not just that the caller is logged in (IDOR/BOLA).
- Role checks happen server-side; hiding a button is not authorization.
- Mass assignment: request bodies are mapped through an allowlist (DTO/schema), not
spread directly into ORM `update()` calls (`role`, `isAdmin`, `tenantId` fields).
- Session/token changes: expiry, rotation, revocation on password change, cookie flags
(`HttpOnly`, `Secure`, `SameSite`).
- Password or token comparison uses constant-time comparison.
## Input handling and injection
- SQL built with string concatenation or template literals instead of parameters.
ORMs' raw-query escape hatches (`$queryRawUnsafe`, `.raw()`, `extra()`) deserve a look.
- Shell commands: `exec`/`shell=True` with interpolated input; prefer argument arrays.
- HTML: `dangerouslySetInnerHTML`, `v-html`, `innerHTML`, `|safe`, `html_safe` with
user-controlled data.
- Paths: user input joined into file paths without normalizing and checking the prefix
(`../` traversal). Archive extraction (zip slip).
- URLs fetched server-side from user input (SSRF): allowlist hosts, block internal
ranges and metadata endpoints.
- Deserialization of untrusted data: `pickle`, `yaml.load` without SafeLoader, Java
native serialization.
- Regex built from user input or with nested quantifiers on user input (ReDoS).
- Validation at the boundary with a schema (Zod, Pydantic, etc.), with size limits.
## Secrets and data exposure
- No credentials, tokens, or private keys in code, tests, fixtures, or config committed.
- Logs do not include passwords, tokens, full card numbers, or unnecessary PII; error
responses do not leak stack traces or SQL to clients.
- API responses do not return more fields than the client needs (password hashes,
internal flags).
- New third-party scripts or SDKs: what data do they receive?
## Error handling and resilience
- No empty `catch` blocks; no `catch (e) { return null }` that turns outages into
wrong answers.
- Errors are either handled or propagated, not both logged-and-rethrown at every level.
- External calls have timeouts. Retries only on idempotent operations, with backoff and
jitter and a cap.
- Resources released on every path: files, connections, locks, subscriptions, timers.
- Partial failure in multi-step operations: is state left consistent? Is there a
compensating action or transaction?
- User-facing errors are actionable; internal errors carry context (IDs, operation).
## Concurrency and async
- Check-then-act sequences on shared state (read balance, then write) without a lock,
transaction, or atomic operation.
- Promises not awaited; `forEach` with async callbacks (does not wait); fire-and-forget
work with no error handling.
- Unbounded `Promise.all` / goroutines / threads over user-sized input; use a
concurrency limit.
- Shared mutable module-level state in servers handling concurrent requests.
- Go: goroutine leaks (no cancellation via context), data races (run `go test -race`),
closing channels from the receiver side.
- Idempotency of webhook and queue consumers: duplicate delivery must not double-apply.
## Data, persistence, and migrations
- Migrations on large tables: adding a column with a volatile default, adding NOT NULL
without a default, creating indexes without `CONCURRENTLY` (Postgres), changing column
types that rewrite the table.
- Deploy ordering: code that reads a new column must not ship before the migration; code
that stops writing an old column must ship before it is dropped (expand/contract).
- Transactions wrap the operations that must be atomic and nothing slow (no HTTP calls
inside a DB transaction).
- Queries return bounded result sets; pagination uses a stable order.
- Unique constraints exist where the code assumes uniqueness.
- Soft-deleted rows excluded where expected.
## Money, time, and numbers
- Money in integer minor units or a decimal type, never binary floats. Rounding mode is
explicit and matches finance requirements.
- Currency stored alongside amounts.
- Timestamps stored in UTC; conversion to local time only at display. DST transitions,
month-end, leap years, and "end of day" in which time zone.
- Integer overflow on counters or IDs (32-bit), division by zero, NaN propagation.
- Comparison of floats with `==`.
## Performance
- Database queries inside loops (N+1); ORM lazy loading in templates or serializers.
- Missing index for a new `WHERE`/`ORDER BY` on a large table.
- Loading entire tables or files into memory when streaming would do.
- Repeated expensive computation that could be hoisted or memoized.
- Synchronous I/O on an event loop (Node `fs.readFileSync` in a request handler,
blocking calls inside `async def`).
- Frontend: large dependencies added to the main bundle; re-renders caused by new
object/array literals passed as props on every render.
## API and contract compatibility
- Removed or renamed fields, endpoints, events, CLI flags, env vars, or config keys.
- Changed semantics with the same shape (a field that used to be cents is now dollars).
- New required request fields break existing clients; new response enum values can
break strict clients.
- Versioning or deprecation path documented; changelog entry for public libraries.
- OpenAPI/GraphQL schema updated alongside the implementation.
## Tests
- New behavior has at least one test that fails without the change.
- Edge cases from the issue and from your adversarial inputs have tests.
- Tests do not depend on order, wall-clock time, network, or randomness without seeds.
- Mocks are limited to boundaries; the code under test is not itself mocked.
- Snapshot updates in the diff were reviewed, not blindly accepted.
- Deleted or skipped tests have a stated reason.
## Frontend specifics
- Loading, empty, and error states handled for every new data fetch.
- Keyboard access and focus management for new interactive elements; labels on inputs;
images have alt text.
- No user-controlled HTML injection; URLs from data sanitized before `href`.
- Effects with missing or excessive dependencies; state derived from props stored in
state (goes stale).
- Keys on lists are stable IDs, not array indexes, when items reorder.
## Language traps
- **JavaScript/TypeScript:** `==` vs `===`; `as` casts and `!` non-null assertions hiding
real nulls; `any` leaking through public types; `parseInt` without radix;
`array.sort()` on numbers without a comparator; `Date` month is zero-based.
- **Python:** mutable default arguments; late-binding closures in loops; bare `except:`;
`is` for value comparison; naive vs. aware datetimes mixed.
- **Go:** ignored errors (`_ =`); loop variable capture in pre-1.22 modules; nil map
writes; `defer` inside loops; returning a typed nil as an interface.
- **Java/Kotlin:** `equals`/`hashCode` mismatch; `Optional.get()` without check;
`!!` in Kotlin on platform types; unclosed streams.
- **SQL:** `NOT IN` with a subquery that can return NULL; implicit casts defeating
indexes; `LIKE '%x'` on large tables.
- **Shell:** unquoted variables; missing `set -euo pipefail`; parsing `ls` output.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.