skills / code

code-review-excellence

what it does

Master effective code review practices to provide constructive feedback, catch bugs early, and foster knowledge sharing while maintaining team morale. Use when reviewing pull requests, establishing review standards, or mentoring developers.

Verified Before and after: 2 models 1 try per case View source Download .zip

Compare the answers

Answers from Claude Sonnet 5.5 and GPT-6.1 Sol · try 1 of 1

Same request, same model, once with the skill and once without. Read both and judge for yourself.

“Can you review this PR? It adds order search to our internal admin API.”

Source material given with the request

PR #418 "Add order search endpoint" Description: Adds GET /orders/search?q=<name>&page=<n>. Page numbers are 1-based, matching the admin UI. 20 results per page. --- a/app/routes/orders.py +++ b/app/routes/orders.py @@ -1,6 +1,25 @@ from flask import Blueprint, request, jsonify from app.db import get_conn +from app.auth import login_required, current_user bp = Blueprint("orders", __name__) + +PAGE_SIZE = 20 + + +@bp.get("/orders/search") +@login_required +def search_orders(): + term = request.args.get("q", "")
Show the rest (12 more lines)
+ page = int(request.args.get("page", 1)) # pages are 1-based in the UI + offset = page * PAGE_SIZE + sql = ( + "SELECT id, customer_name, total FROM orders " + f"WHERE account_id = {current_user.account_id} " + f"AND customer_name ILIKE '%{term}%' " + f"ORDER BY created_at DESC LIMIT {PAGE_SIZE} OFFSET {offset}" + ) + with get_conn() as conn: + rows = conn.execute(sql).fetchall() + return jsonify([dict(r) for r in rows])
with the skill
## Summary Adds `GET /orders/search` with auth, account scoping, and pagination. Scoping by `account_id` and requiring login are the right instincts. However, there is a SQL injection and a pagination bug that both need fixing before merge. ## Strengths - 🎉 Results are scoped to `current_user.account_id`, so tenants can't see each other's orders. - 🎉 The endpoint is behind `@login_required`, and the 1-based paging is documented in the PR description and a code comment. ## Required Changes 🔴 **[blocking] SQL injection via `q`.** `term` goes straight into the query through an f-string. Something like `q=%'; DROP TABLE orders; --` or a `UNION SELECT` against other tables is possible, and because this is an admin API the blast radius is large. It also bypasses the `account_id` scoping, since an attacker can add `OR 1=1`. Please use bound parameters. `account_id` and the pagination values should be parameterized too. They're likely ints today, but this keeps the query safe if that changes. 🔴 **[blocking] Off-by-one in pagination.** The PR says pages are 1-based, but `offset = page * PAGE_SIZE` means page 1 starts at row 20, so the first 20 results are never reachable. It should be `(page - 1) * PAGE_SIZE`. 🔴 **[blocking] Unvalidated `page`.** - `?page=abc` raises `ValueError`, which becomes a 500. - `?page=0` (or negative) gives a negative offset once the fix above is in, and Postgres errors on that, also a 500. Using `request.args.get("page", 1, type=int)` and clamping to `>= 1` (or returning a 400) would handle both.
Show the rest (55 more lines)
## Suggested fix ```python @bp.get("/orders/search") @login_required def search_orders(): term = request.args.get("q", "") page = max(request.args.get("page", 1, type=int), 1) offset = (page - 1) * PAGE_SIZE # Escape LIKE wildcards so users searching "50%_off" get literal matches escaped = term.replace("\\", "\\\\").replace("%", r"\%").replace("_", r"\_") sql = ( "SELECT id, customer_name, total FROM orders " "WHERE account_id = %s AND customer_name ILIKE %s " "ORDER BY created_at DESC, id DESC " "LIMIT %s OFFSET %s" ) params = (current_user.account_id, f"%{escaped}%", PAGE_SIZE, offset) with get_conn() as conn: rows = conn.execute(sql, params).fetchall() return jsonify([dict(r) for r in rows]) ``` The `%s` placeholder style assumes psycopg. If `get_conn` wraps SQLAlchemy or another driver, use that driver's style, such as `text()` with named params. ## Suggestions 💡 **[important] Stable ordering.** `ORDER BY created_at DESC` alone is non-deterministic when timestamps tie, so rows can repeat or vanish across pages. I added `id DESC` as a tiebreaker in the fix above. 💡 **Pagination metadata.** The response is a bare list, so the UI can't tell whether there's a next page without making an extra request. Consider returning `{"results": [...], "page": n, "page_size": 20}`, plus `has_more` (fetch `PAGE_SIZE + 1` rows) or a total count if the UI needs one. 💡 **Cap on `page`.** Very large offsets are slow in Postgres. A sane upper bound, or keyset pagination later, is worth considering. ## Questions ❓ Is `@login_required` enough for an admin API, or should this check a role/permission (e.g. `admin_required`)? I'm not sure how `current_user` is modeled here. ❓ What should an empty `q` do? Currently it returns all of the account's orders. That may be fine, but if not, return a 400 or require a minimum length. ❓ Is `customer_name` indexed? `ILIKE '%term%'` can't use a btree index, so on a large `orders` table this will seq-scan. A `pg_trgm` GIN index would fix that. It's not blocking if the table is small. ❓ Could we add tests covering: - page 1 returns the first 20 rows - page 2 returns the next 20 - invalid or zero `page` values - a `q` containing a quote or `%` - one account can't see another account's orders The cross-account and injection tests would have caught the first two issues here. ## Verdict 🔄 **Request changes.** Fix the injection, the offset math, and the `page` validation. The rest is non-blocking. Happy to pair on the parameterization if the DB wrapper makes it awkward.
without
# Review of PR #418: Add order search endpoint I'd block this until the first two issues are fixed. ## Blocking **1. SQL injection via `q`** `term` goes straight into the SQL string via an f-string. Even on an internal admin API, this lets any logged-in user run arbitrary SQL. For example, `q=%' OR 1=1 --` removes the `account_id` filter and returns every tenant's orders, and a `UNION SELECT` could read other tables. `current_user.account_id` is also interpolated. It's probably safe today, but it should be parameterized too, so nobody has to reason about where each value came from. **2. Pagination off-by-one** The comment says pages are 1-based, but `offset = page * PAGE_SIZE` means page 1 gives `OFFSET 20`. The first 20 results are unreachable, and the admin UI would always start on what is really the second page. It should be: ```python offset = (page - 1) * PAGE_SIZE ```
Show the rest (58 more lines)
## Should fix **3. Input validation on `page`** - `int(...)` raises `ValueError` on `?page=abc`, which gives a 500 instead of a 400. - `page=0` or a negative value produces a negative offset, which Postgres rejects with an error (another 500). - Consider an upper bound too, since very large offsets are slow. **4. Unstable ordering** `ORDER BY created_at DESC` isn't unique. Rows with identical timestamps can appear on two pages or on none. Add a tiebreaker: `ORDER BY created_at DESC, id DESC`. **5. LIKE wildcards in user input** `%` and `_` in `q` act as wildcards. This isn't a security problem once the query is parameterized, but searching for `50%_off` won't behave as expected. Escape them if exact substring matching matters. ## Nice to have - Return pagination metadata (`page`, `page_size`, maybe `has_more`) instead of a bare list, so the UI knows when to stop. - Cap the length of `q`. - Add tests for: page 1 returning the newest rows, an invalid page returning 400, a quote character in `q`, and results being scoped to the user's account (no cross-account leakage). ## Suggested fix This assumes a DB-API driver like psycopg. Adjust the placeholder style if `get_conn` wraps something else, such as SQLAlchemy `text()` with named params. ```python PAGE_SIZE = 20 MAX_PAGE = 10_000 def _escape_like(s: str) -> str: return s.replace("\\", "\\\\").replace("%", r"\%").replace("_", r"\_") @bp.get("/orders/search") @login_required def search_orders(): term = request.args.get("q", "")[:100] try: page = int(request.args.get("page", 1)) except ValueError: return jsonify(error="page must be an integer"), 400 if not 1 <= page <= MAX_PAGE: return jsonify(error=f"page must be between 1 and {MAX_PAGE}"), 400 offset = (page - 1) * PAGE_SIZE sql = ( "SELECT id, customer_name, total FROM orders " "WHERE account_id = %s AND customer_name ILIKE %s " "ORDER BY created_at DESC, id DESC " "LIMIT %s OFFSET %s" ) params = (current_user.account_id, f"%{_escape_like(term)}%", PAGE_SIZE, offset) with get_conn() as conn: rows = conn.execute(sql, params).fetchall() return jsonify([dict(r) for r in rows]) ``` Also, if other routes in this codebase build SQL with f-strings, it's worth a follow-up audit.

Answers are shown exactly as the model wrote them, from the first try, and nothing is picked by hand.

The models

2 models

Claude Sonnet 5.5

made by Anthropic

claude-sonnet-5-5 · revision not reported by the provider

1 try per case · tested 3 Nov 2026

GPT-6.1 Sol

made by OpenAI

gpt-6.1-sol · revision not reported by the provider

1 try per case · tested 3 Nov 2026

The tasks

written by Carte Blanche
py-chunked-clean-diff

“Quick review before I merge? Mostly want to know if anything blocks it.”

py-search-injection-pagination

“Can you review this PR? It adds order search to our internal admin API.”

py-wallet-withdraw-race

“Please review this change to our wallet service.”

ts-async-foreach-import

“Review this before I merge please. It's the CSV contact import we talked about.”

ts-file-delete-first-pr

“This is Priya's first PR since she joined the team last week. Can you review it? I'll paste your comments into GitHub.”

Install

Download the .zip and unzip it. Put its files, in a folder named code-review-excellence, here:

Claude Code~/.claude/skills/Every project. For one project only, use .claude/skills/ inside it.
Codex~/.agents/skills/Every project. For one repository only, use .agents/skills/ inside it.
Cursor~/.cursor/skills/Cursor also reads the Claude Code and Codex folders above.

Then start a new session and ask for what the skill does. Other agents that read SKILL.md have their own skills folder; check their docs.

Coming after launch: get skills from inside your agent, no download needed.