GitHub Copilot CLI Code Review Tutorial

GitHub Copilot CLIAcademy lesson 62Cluster 5 · Lesson 11 of 12Intermediate14 min readVersion-sensitive
Published
Updated
Last technically verified
GitHub Copilot CLI Code Review TutorialGitHub Copilot CLI11Intermediate/github-copilot/cli/code-review/

/review runs a read-only agent over your changes and reports what it finds. It is genuinely useful and it is not a gate. Understanding the difference is the point of this lesson, because a review tool trusted beyond its reach is worse than no review tool — it converts “nobody looked at this” into “something looked at this”, and only one of those prompts a human to look.

What the commands do

/review runs the built-in code-review agent, which GitHub documents as analysing changes and focusing on substantive issues. It is read-only.

/security-review analyses staged and unstaged changes specifically for security vulnerabilities.

/diff is not review — it shows changes made in the current directory. Worth running first, because reading your own diff is the cheapest review available.

/rubber-duck gives an independent critique using a different model.

The loop

Review as a stage, not a verdict

Note where /review sits: after tests and linting, not instead of them. A linter is deterministic and a test suite is objective. Review is neither, so it belongs after the checks that are, catching the class those cannot — the logic that is syntactically fine and semantically wrong.

Build something to review

Review on a clean diff teaches nothing. Plant flaws deliberately.

import sqlite3


def get_user(conn, user_id):
    # Flaw 1: string-formatted SQL
    cur = conn.execute(f"SELECT * FROM users WHERE id = {user_id}")
    return cur.fetchone()


def parse_age(value):
    # Flaw 2: swallows every exception, returns a misleading default
    try:
        return int(value)
    except Exception:
        return 0


def apply_discount(total, percent):
    # Flaw 3: no bounds check — percent > 100 produces a negative total
    return total - (total * percent / 100)


def is_adult(age):
    # Flaw 4: off-by-one
    return age > 18

Four flaws of different kinds, which is the point: they exercise different parts of what review can and cannot see.

  • SQL injection — a well-known pattern, textually local.
  • Overly broad exception handling — a code smell with real consequences.
  • Missing bounds check — logic, requiring reasoning about values.
  • Off-by-one — correct-looking code that is wrong only if you know the intended rule.

Practical example

Confirming the planted flaws are real

Establish ground truth before reviewing, so the review can be scored against known defects rather than judged on how convincing it sounds.

Status
Tested implementation
Runtime
Python 3.12
Command
python -m pytest -q && python -c 'from app import apply_discount, parse_age, is_adult; print(apply_discount(100,150), parse_age("abc"), is_adult(18))'
Result
3 passed in 0.02s — then: -50.0 0 False
Run on
August 23, 2026

The three tests pass while three of the four flaws are live, which is the point worth absorbing before running any review tool.

apply_discount(100, 150) returns -50.0 — a discount larger than the total produces a negative price. parse_age("abc") returns 0 rather than raising, so invalid input becomes a plausible-looking age. is_adult(18) returns False, because age > 18 excludes the boundary the name implies.

A green suite establishes that the code does what the tests check. It says nothing about the three defects the tests do not cover.

Run the review

git init && git add -A

Then in a session:

/review

Practical example

Reviewing a deliberately flawed module

Establish which categories of defect the review agent surfaces and which it passes over.

Status
Example implementation
Not executed here
This code was written against the documentation cited at the end of the lesson but was not run while writing it. Treat the commands as the ones to run, not as output that has been observed.

Run /review against the four planted flaws, then check each against a prediction made in advance:

  • Flaw 1, SQL injection — a recognisable pattern; expect it to be found.
  • Flaw 2, broad except — a common smell; expect it to be found.
  • Flaw 3, missing bounds check — requires reasoning about values; less certain.
  • Flaw 4, off-by-one — requires knowing the intended rule, which is nowhere in the code; least certain.

Writing the prediction down before running the review is what makes this informative. Reading the output first turns everything into “yes, that seems right”.

Reading findings properly

Every finding is a hypothesis. Three questions decide what to do with it.

Is it real? Read the line. Models produce confident, well-written descriptions of bugs that are not there, and fluency is not evidence.

Does it matter here? SQL injection in production code is critical. The same pattern in a test fixture with a hard-coded integer is noise.

Is the suggested fix right? A correct diagnosis can come with a wrong remedy — catching a narrower exception is right; catching it and returning 0 is the original bug in a smaller costume.

Copilot prompt

For the SQL injection finding: quote the exact line, explain how an attacker would exploit it given that user_id comes from an HTTP query parameter, and show the parameterised version.

If you cannot demonstrate the exploit concretely, say so.

That last sentence is the useful part. Asking for a concrete demonstration is a reliable way to separate real findings from pattern-matched ones, because a finding that cannot be demonstrated usually cannot be reproduced either.

What review is good at

Local, textual patterns. String-formatted SQL, bare except, hard-coded credentials, disabled TLS verification. Pattern recognition on a specific line.

Common mistakes in common frameworks. Missing await, unclosed resources, mutable default arguments.

Consistency within a diff. One function validating input while its neighbour does not.

Explaining a change. “What does this diff do” is a question models answer well, and it is useful before reviewing a colleague’s work.

What review misses

Architectural problems. A change can be locally perfect and structurally wrong. Review sees a diff.

Anything depending on unseen context. Whether this endpoint is public, whether that value is user-controlled, whether the table has a million rows. The agent reasons from code; the context is not in the code.

Business logic errors. Flaw 4 above is the example. age > 18 is correct code for one rule and wrong for another, and nothing in the file says which.

Absences. Missing authorisation, missing rate limiting, a missing test. Review looks at what is there.

Its own blind spots. Reviewing agent-written code with the same model is asking it to find mistakes of a kind it does not make by construction.

Security review

/security-review is worth running whenever a change touches input handling, authentication, subprocess execution, deserialisation, file paths, or anything constructing a query.

Scoping a review

An unscoped review of a large diff produces a long list of shallow observations. Narrowing the scope produces fewer, better findings, because the agent’s attention is finite in the same way yours is.

Three ways to narrow, in increasing usefulness.

By path. Point it at the directory that matters rather than the whole change. A diff touching source, tests, documentation and lockfiles has one part worth reviewing carefully.

By concern. Ask for one category at a time — error handling, then input validation, then resource lifecycle. Three focused passes consistently beat one general pass, and the results are easier to act on because each pass has a theme.

By risk. Ask which parts of the diff carry the most risk, then review those properly. This works well as an opening move on an unfamiliar branch.

Copilot prompt

Review only the changes under src/api/ on this branch.

Look at one thing: how user-supplied input reaches the database or the filesystem. Ignore style, naming and test coverage.

For each path from input to a sink, tell me what validation exists between them.

“Ignore style, naming and test coverage” is doing as much work as the positive instruction. Without it, a meaningful fraction of the output will be about naming, and the signal you wanted gets diluted by comments nobody will act on.

False positives, and the cost of them

False positives in AI review are not merely noise. They have a specific corrosive effect: a reviewer who has dismissed six spurious findings dismisses the seventh without reading it, and the seventh is sometimes real.

This is the same attention economics as the approval prompts in the pillar, and the same remedy applies — reduce the volume so the remainder is worth reading.

The patterns that generate the most false positives are worth recognising:

Pattern matching without context. String formatting in SQL is a genuine flaw in application code and irrelevant in a migration script with literal values.

Assuming untrusted input. A function that does not validate its argument is fine when every caller is internal and validated upstream. The agent cannot see the callers unless it read them.

Applying general rules to specific decisions. A broad except is usually wrong and is exactly right in a top-level handler whose job is to log and continue.

Reporting missing tests that exist elsewhere. A review scoped to a diff does not see the test file that was not touched.

Reviewing someone else’s changes

/review is not only for your own work. Reviewing a colleague’s branch — or a dependency update — is often more valuable, because you lack the context that makes your own diff feel obvious.

Copilot prompt

Review the changes on this branch against main.

Do not modify anything.

For each finding: file, line, what could go wrong, and how confident you are. Separate what you verified from what you inferred.

Then tell me which parts of this diff you found hardest to reason about.

That final question is underrated. The parts an agent found hard to follow are a reasonable proxy for the parts a human reviewer will skim, and they are where defects survive review. It is a signal you cannot easily get any other way.

Iterating

After fixing, review again — but read the second run sceptically. An agent that has seen its own findings addressed tends towards agreement, and “all previous issues resolved” is a claim, not a verification. Re-run the tests too.

Where this fits

Review is one layer:

Tests verify behaviour you specified. Objective, and blind to what you did not think to test.

Static analysis finds defined patterns. Deterministic, no judgement.

Type checking enforces consistency. Cheap, and narrow.

AI review finds things without defined rules. Broad, and probabilistic.

Human review brings context, intent and accountability. Nothing else has these.

AI review’s distinctive contribution is breadth without configuration — it will comment on things nobody wrote a rule for. Its weakness is that it cannot tell you when it missed something.

Review of AI-written code specifically

Most of this cluster is about an agent writing code. That changes what review should look for, because the mistakes differ from the ones humans make.

Plausible-but-wrong APIs. Generated code sometimes calls a method that does not exist, or exists with different arguments in a different version. Tests catch this immediately if the path is covered and never if it is not.

Silent scope expansion. A change that also renamed something, reformatted a file, or “improved” an unrelated function. git diff --stat catches this faster than reading.

Tests that mirror the implementation. Generated tests trend towards asserting that the code does what the code does. They pass, and they verify nothing. The check is to break the implementation and confirm the test fails.

Inconsistent error handling. Two functions written in the same session handling failure differently — one raising, one returning a sentinel. Each is defensible; together they are a bug waiting for a caller.

Confident comments that no longer match. A comment describing the previous behaviour, left in place after the logic changed underneath it.

Making review part of the loop rather than a ritual

A review step that runs and is skimmed is worse than no review step, because it provides cover. Three things make it real.

Act on findings immediately or record why not. A finding you looked at and decided was a false positive is closed. A finding you scrolled past is still open and you have forgotten it. If a finding is wrong, saying so — in the pull request, in a comment — is what stops it being raised again next week.

Track what it catches over time. After a month you will know whether your review step finds real problems on your codebase or produces noise. That is a question with an answer, and the answer should determine how much weight the step carries. Nobody usually asks it.

Keep the scope tight enough that the output is short. A review producing forty observations gets skimmed; one producing four gets read. The narrowing techniques above are not about getting better findings so much as getting a number of findings a human will actually process.

What to carry forward

Read your own diff first. It is the cheapest review available and it catches the scope expansion that everything else misses.

Run /review after tests and linting, not instead.

Verify every finding. Confidence and correctness are unrelated in model output.

Ask for a concrete demonstration to separate real findings from pattern matches.

Use /rubber-duck on agent-written code, because a different model has different blind spots.

Never treat a clean review as a gate. It means nothing was found, which is a different claim from nothing being there.

One last framing. The right mental model for /review is a well-read colleague skimming your diff who has never seen the rest of the system, does not know what the code is for, and will not tell you when they were unsure. That colleague is worth having — they catch real things, quickly, at no cost. They are not a substitute for someone who knows the system, and no amount of prompting turns them into one.

A calibration exercise worth repeating

Once a quarter, plant a known defect in a branch and run your review setup against it. Not to test the model, which changes underneath you, but to test whether your process still catches things.

Pick a defect of the kind that would actually hurt you — an authorisation check removed, an input path that stops being validated, a resource that stops being closed. Run the review. See whether it surfaces, and whether anyone acts on it.

The result tells you something a passing review never does. A setup that finds a planted defect is worth the attention it consumes. One that misses it is providing reassurance rather than review, and reassurance is the thing you most want to avoid buying.

Next

GitHub Actions is the capstone: automating review so it runs on every pull request, with the tool restrictions and token scoping that makes unattended review safe.

Custom agents covers building a reviewer that encodes your team’s specific concerns, and Cluster 1’s best practices covers the verification mindset this lesson applies to review specifically.

Sources

Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.

Primary sources