Semi-manual code review of a GitHub PR, topic by topic, in a lean local UI. Claude reads the diff, groups the business-logic changes into topics (risks first, then one per acceptance criterion / concept), picks short line ranges for each, and opens review.py which shows a short PR summary, ticket digest, check status, existing comments and the highlighted snippets on one scrolling page. The user selects lines to leave a comment, change request or question, then Submits; Claude posts the revie...
Installs into .claude/skills of the current project.
Are you the author of Human Code Review?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/sezaakgun-human-code-review)
---
name: human-code-review
description: Semi-manual code review of a GitHub PR, topic by topic, in a lean local UI. Claude reads the diff, groups the business-logic changes into topics (risks first, then one per acceptance criterion / concept), picks short line ranges for each, and opens review.py which shows a short PR summary, ticket digest, check status, existing comments and the highlighted snippets on one scrolling page. The user selects lines to leave a comment, change request or question, then Submits; Claude posts the review to GitHub and answers the questions. Use when the user says "human code review", "triage review", "semi manual review", "review this pr with me", or "/human-code-review <PR#>". "/human-code-review learn" builds or refreshes the repo file from past review threads.
---
# human-code-review
Claude filters, groups and annotates; the user reads, judges and asks. `review.py` is the UI, stdlib only.
## Steps
Work dir is `~/.cache/human-code-review/<repo>-<pr>/`, one per PR; never share a dir between two reviews.
0. Configure, first run only. If `~/.config/human-code-review/config.json` does not exist, ask the user two things, then `mkdir -p ~/.config/human-code-review` and write it:
- issue tracker: `jira`, `linear`, `github-issues`, or `none`
- how to fetch a ticket: an MCP tool name, a CLI command with `{key}` in it, or a URL pattern
```json
{ "tracker": "jira", "keyPattern": "[A-Z][A-Z0-9]+-\\d+", "fetch": "mcp: getJiraIssue", "asked": { "owner/repo": "2026-09-10" } }
```
`keyPattern` defaults per tracker (`#\\d+` for github-issues). With `none`, skip step 4 without asking. Never hardcode a tracker in this skill.
1. Resolve the PR number. Read the whole diff yourself. Ignore formatting, imports, doc comments, docs, tests, env plumbing, renames.
1b. Repo file, asked now and then. If the repo root has no `human-code-review.md` and `asked[<owner/repo>]` in `~/.config/human-code-review/config.json` is missing or older than 30 days, stop and ask the user with a question tool: "No review file for this repo. Build one from past review threads? 1, 3, 6 or 12 months back, or skip." Do not decide for them, do not defer it to the summary. Write today's date under `asked` only after they answer. If the file exists but its git last-commit date is older than 90 days, ask the same way about a refresh. `/human-code-review learn` always works on demand. On a months answer:
```sh
uv run ${CLAUDE_PLUGIN_ROOT}/skills/human-code-review/mine.py --repo <owner/name> --months <n> --out ~/.cache/human-code-review/<repo>-learn
```
Read `digest.md` (and `threads.json` for the full text where a line is cut). Write `human-code-review.md` in the repo root with only what RULES.md leaves to the repo: which paths are config, generated or boilerplate; repo-specific instances of rules 1 to 8 with the file or pattern they apply to; which paths need a sign-off from a specific role or a paired PR in another repo; the language reviewers write in and two or three of their own phrasings. Never name a person: no GitHub logins, no names, no per-reviewer counts — describe roles and paths only, and strip any author or reviewer handle from quoted phrasings. Under 60 lines. Show it to the user before the review continues; they commit it when they like it. `/human-code-review learn [months]` runs only this step, for a first setup or a refresh. With zero human threads say so and write nothing.
2. Read `RULES.md` in this folder first: 8 "always show" rules, 7 "show as checked", the skip list, and the comment style. If the repo root has `human-code-review.md`, read it too; it only adds path names and repo-specific instances, never overrides RULES.md. Tag each topic with the rule number that put it there (`"rule": "3 · contract vs code"`). Suggested comments under each rule may be offered in the note as "you could say: …", in the language the reviewer uses on the PR.
Before writing a topic that claims a call site, a caller or an entry point is wrong, **trace it** —
which route reaches it, which version, which consumer. A topic built from the diff alone is a guess,
and a wrong one costs more trust than a missed one. End each topic note with what you actually
verified and what you only inferred, so the reader can weigh it.
Being internally consistent and covered by tests is not the same as being right: the bucket test is
whether a reader could disagree, not whether the code contradicts itself.
The page exists to shrink what a human reads, and the only failure that matters is a change a human should have seen but did not. So the bucket test is: could a reader with the ticket in mind disagree with this change or with Claude's judgement of it? If yes, it is a topic. If Claude is unsure whether it is safe, it is a topic. Checked is for things Claude verified mechanically and would bet on.
Decide, for every change, one of three buckets:
- **needs a human** → a topic. Only when the reader must judge: a business decision, a performance trade-off, a likely bug, or something that does not fit how the repo does things.
- **checked** → one line. Claude verified it against the repo rules and it is fine. The reader skims, never opens code.
- **skipped** → a count. Formatting, imports, doc comments, docs, tests, env plumbing, renames.
Write `~/.cache/human-code-review/<repo>-<pr>/topics.json`:
```json
{ "verdict": { "event": "APPROVE|COMMENT|REQUEST_CHANGES", "why": "one sentence" },
"skipped": "34 test files, 13 docs, ~90 formatting hunks",
"checked": [ { "what": "Cache key", "why": "has tenant, locale and every filter", "paths": ["src/…"] } ],
"topics": [ { "title": "short", "kind": "risk|perf|decision|convention",
"note": "≤3 plain sentences, end with the question the reader must answer",
"snippets": [ { "path": "src/…", "lines": [start, end] } ] } ] }
```
Order topics: risk, perf, decision, convention. Keep every snippet under ~25 lines. Line numbers are new-file lines. A 1000-line PR should produce 3–8 topics; if you have more, some belong in checked.
3. Write `~/.cache/human-code-review/<repo>-<pr>/pr-summary.md`: 4–6 bullets in markdown, what changed and what a reviewer must know (flags, paired PRs, known gaps).
4. If the PR title, branch or body matches `keyPattern` from the config, fetch the ticket the way `fetch` says and write `~/.cache/human-code-review/<repo>-<pr>/ticket.json` as `{ "key", "summary", "description" }` where `description` is a markdown digest: need, acceptance criteria bullets, open questions, and a short digest of test comments on the ticket. Skip silently if the tracker is `none` or the fetch fails.
5. Open the UI in the background and end the turn:
```sh
uv run ${CLAUDE_PLUGIN_ROOT}/skills/human-code-review/review.py --pr <n> --repo <owner/name> \
--topics ~/.cache/human-code-review/<repo>-<pr>/topics.json --pr-summary ~/.cache/human-code-review/<repo>-<pr>/pr-summary.md \
[--ticket ~/.cache/human-code-review/<repo>-<pr>/ticket.json] --src /tmp/pr<n> --out ~/.cache/human-code-review/<repo>-<pr>/result.json
```
`--src` is a worktree of the PR head; a snippet on a file the PR does not change is read from there and labelled "unchanged, shown for context". Use it when the risk lives in shared code the diff relies on. Always pass `--repo`; the process may not inherit the repo cwd. It blocks until Submit, prints the result JSON, writes it to `--out`, exits.
Then check the page rendered without a browser: `uv run ${CLAUDE_PLUGIN_ROOT}/skills/human-code-review/selfcheck.py http://127.0.0.1:<port>/`. It fails on an empty topic list or a dead server. Never open a browser or Playwright for verification; the user opens the page.
In the **same turn**, also start the question waiter in the background:
```sh
sh ${CLAUDE_PLUGIN_ROOT}/skills/human-code-review/wait-question.sh ~/.cache/human-code-review/<repo>-<pr>
```
While it runs it touches `.listening` in the work dir every second; the page shows a warning bar when that heartbeat is older than 5 s, so the user knows Claude is not there. Always keep a waiter running while a review is open. It exits within a second when the user either asks a question or presses Submit. On Submit it prints `SUBMIT` and the
result JSON: post the review (step 6) and stop; do not wait for the server to exit.
`{"status":"superseded"}` means a newer waiter owns this review — stop silently, post nothing.
`{"status":"closed"}` means the reader pressed End review — stop, and post nothing unless a result
was already written. On a question it prints one JSON line per new question
(`{id, path, line, start_line?, body, thread?}`; `path` is null for a question about the whole PR). A line with `thread` is a follow-up in an existing thread: read the earlier turns (the root and every line in `questions.jsonl` with that `thread`, plus their `answers/<id>.md`) and answer in that context; the page shows the thread as alternating you/claude turns with a reply box under the latest answer. When it wakes you: read the snippet context, answer in 2–6 short
markdown lines, write the answer to `~/.cache/human-code-review/<repo>-<pr>/answers/<id>.md`, then restart the waiter. The page
polls `/answers` and patches only that question's row. Do not reply in chat for live questions unless the
answer needs a tool result the user must see. Keep answering until the review server exits.
6. Read the result:
```json
{ "verdict": "APPROVE|COMMENT|REQUEST_CHANGES", "general": "...", "repo": "...", "pr": 123,
"comments": [ { "path", "line", "start_line"?, "side": "RIGHT|LEFT", "kind": "comment|change|question", "body" } ] }
```
- `comment` and `change` → inline review comments, body verbatim (`change` bodies prefixed with **Change request:**). Include `start_line`/`start_side` when present.
- `question` → answer in chat with file:line context. Never post to GitHub.
- `general` starting with `?` → a question for Claude; otherwise the review body.
- Post with the helper, never with raw `gh` — it claims `posted.json` atomically before calling
the API, so two sessions waiting on one review cannot both post it:
```sh
uv run ${CLAUDE_PLUGIN_ROOT}/skills/human-code-review/post-review.py ~/.cache/human-code-review/<repo>-<pr>
```
It posts one review with `event` = verdict, `body` and `comments[]`, writes the URL into
`posted.json`, and prints a summary. Exit 3 means another session already claimed it — say so
and post nothing. `status: nothing-to-post` means the verdict was COMMENT with no content; say
so. Questions come back in `questions` for you to answer in chat, and are never posted.
`--dry-run` shows the payload without claiming anything.
7. `post-review.py` writes the URL into `posted.json` itself, and the page picks it up; the server stays up 10 minutes after Submit for this. Reply with the review link, then the answers to each question.
## Writing rules for notes and summary
- Plain words. Short sentences. One fact per sentence.
- No arrows, no abbreviations the reader has to decode, no code identifiers unless the reader must search for them.
- A topic note is at most 3 sentences: what it does, why it matters, what to do (if anything).
- A risk note says the problem first, then the cost, then the fix.
- The PR summary is 4–6 bullets a product manager could read.
## Re-review
When the user has a previous review on the PR (the server reports it; also `gh api repos/{repo}/pulls/{pr}/reviews`), build a `since` list in topics.json and it renders above "needs you":
```json
"since": [ { "title": "your comment, shortened", "status": "addressed|partly|replied|open|new",
"resolution": "ok|needs you",
"note": "what the author replied, what changed in code, and whether that settles it",
"snippets": [ { "path": "…", "lines": [a, b] } ] } ]
```
`resolution: ok` only when the comment was handled and nothing new emerged around it; the item renders green and starts collapsed, and the note begins with "Ok." Otherwise `needs you` and the note begins with "Needs you." followed by the open question. One item per comment the user left (read the thread: their words, the author's reply, and `git diff <reviewed sha>..<head>` on that file). Then one item per other change since the review that a human should see, status `new`. Snippet rows come from the since-diff first, so they show the actual change. The raw diff since the review stays available, collapsed, at the top. Read state is keyed by topic title and survives regeneration; a read topic flips back to unread when one of its files changed since you last opened the page, or a thread in it got a new reply. Keep topic titles stable across regenerations of the same PR.
## Recovery
Run `--status` on the work dir before anything else. `"result": "unposted"` means the user already
submitted and nothing was posted: post it first (step 6), then continue. Drafts stay in the browser until `posted.json` appears, so a missed post is visible on the page.
## Rules
- Several sessions may run this skill at once on one machine. Never stop a server or waiter by pattern (`pkill -f review.py`, `pkill -f wait-question.sh`): that kills other reviews. Stop only your own background task by its id.
- You never need to hunt for a process anyway. Starting the server attaches to a same-build one or
retires a stale-build one by itself, scoped to the work dir, so it can never touch another PR's
review. Ending a review is the reader's job: the End review button in the page footer.
- The footer shows the build the page was rendered from. A server serves one page for its whole
life, so a page from an older build cannot pick up edits to `review.py` — restarting is what
applies them, and the page warns when it has been superseded.
- Never rewrite the user's wording. Never approve or request changes without a result file that says so.
- Drafts and read state live in the browser's localStorage per PR until Submit.