Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Note

This was 100% written by Claude without even a looksy at the code. This is a POC.

PR Reviewer

A local tool for reading GitHub pull requests and asking Claude about them.

It renders a PR the way GitHub does — the whole diff, or one commit at a time — and lets you ask questions anchored to a specific line. Claude answers with the PR's repository checked out in front of it, so it can grep, read call sites, and run git.

Questions and answers are kept on this machine, in a folder the tool owns, so a server restart doesn't lose them. They are never posted to GitHub. The tool has no code path that writes to GitHub at all — gh is only ever called as gh api <path>.

What you need

Four things, all of which you probably already have:

  • uv — runs the app. It resolves the Python dependencies into an ephemeral environment, and will download a suitable Python itself if you don't have one (the project needs 3.11 or newer). There is nothing to pip install.
  • git — used to clone the repository under review, keep a worktree at the PR head, and produce the diffs the page is built from.
  • GitHub CLI (gh), logged in — the only way the tool talks to GitHub. Install it, then run gh auth login once. The account you log in as needs read access to the repositories whose PRs you open, private ones included. gh is only ever invoked as gh api <path>, so it never writes anything back.
  • A Claude login — the Claude Code CLI ships bundled with the claude-agent-sdk dependency, so there is nothing extra to install, but it needs credentials. If you already use Claude Code, you're done; otherwise run claude once and log in, or set ANTHROPIC_API_KEY in the environment you start the server from. Answers are billed to that account like any other Claude Code usage.

Everything above is enough for the whole tool except the Walkthrough button, which invokes the dev-utils:explain-diff-html skill and so needs the dev-utils plugin installed in Claude Code. The rest of the app does not care whether it is there; the button says so and stops if it isn't. PR_REVIEWER_PLUGIN_DIR points at the plugin directory if it lives somewhere the app doesn't think to look.

Running it

uvx --from /path/to/pr_reviewer pr-reviewer            # default port 5173
uvx --from /path/to/pr_reviewer pr-reviewer --port 5199

Or from inside the project directory: uv run pr-reviewer. Then open http://127.0.0.1:5173 and paste a PR URL.

The folder is self-contained — copy it anywhere, or push it to a repo and uvx --from git+https://… pr-reviewer.

Using it

  • Commits steps through the PR commit by commit. Each commit gets a short orientation note from Claude, generated in the background; whichever commit you open jumps the queue.
  • All files shows the whole PR, merge-base to head.
  • Hover any line and click + to ask about it. Shift-click a second line to ask about a range. Follow-ups continue the same conversation.
  • Ask about PR opens a drawer for questions about the pull request as a whole. It stays open and keeps its answers as you step between commits.
  • collapses a question thread, × deletes it. Deleting clears it from your view, from the server and from memory.json; it does not remove it from Claude's session, which has already seen it.
  • at a hunk boundary reveals more of the file, 40 lines at a time.
  • on a Python line opens the trace view in a new tab: the whole file at the PR head, centred on that line's function, with its callers as code on the left and everything it calls on the right. Everything off the trace is dimmed. Click any function name to re-centre on it and walk the chain; the toggle in the topbar switches a click to a popup of the function's call sites instead, each openable at that line in a new tab. The call index is built in the background when a PR is opened — pure static analysis (ast + jedi), no Claude, no tokens — and shows only calls that resolve to exactly one definition in the repository, never a guess.
  • Walkthrough has Claude write a guided explanation of the whole PR — background, the core intuition, a tour of the changes and a short quiz — as a self-contained HTML page that opens in a new tab. It takes a few minutes and costs tokens, so it only ever runs when you click. Push new commits and the button offers to write a fresh one; the page written for the old head stays readable in the meantime, since it still explains the commits it was written for.
  • Split toggles side-by-side. Questions lists every thread in one place.
  • Tests lists the tests the PR introduces — every test function whose def line the diff adds — as a table of class-qualified name and docstring, each linking to its file in the whole-PR diff. A test that already existed and only had its body edited is not one the PR introduces, so it is not listed. Pure ast, no Claude, and read from the diff on each visit, so a Refresh that brings in new commits brings in their tests.
  • Refresh fetches the PR again, so commits pushed since you opened it show up. Your questions stay; notes you already paid for are not regenerated. A question anchored to a commit that was rebased away moves to the whole-PR diff rather than disappearing.
  • Keyboard: n/p for next/previous commit, j/k to move between files.

How it fits together

Module Job
app.py Flask routes, diff fragments, the SSE event stream
github.py gh wrapper — PR URL parsing, PR metadata, commit list
repo.py clone cache, PR refs, worktree, diffs, file reads at a revision
diff.py git diff output → files → hunks → lines (pure parsing)
render.py highlighting, gaps, file tree — the model a template can loop over
trace.py the call-trace index — ast scan, jedi resolution, graph, cache
testcases.py the tests a PR introduces — added lines crossed with an ast scan
session.py the persistent Claude session and the question queue
summaries.py background per-commit context generation
walkthrough.py the one write-capable run: the skill, its gate, its output
store.py review state — threads, turns, summaries, the event bus
memory.py memory.json — that state, saved and restored
paths.py where the persistence folder is
disk.py what that folder is holding, its size, and removing parts of it

The repository is cloned once per project into the persistence folder (see below) with --filter=blob:none, so even a very large repo opens in seconds; blobs are fetched as they are needed. A detached worktree at the PR head is what Claude works in.

What the session can reach

The PR under review is untrusted input — its diff, description and commit messages all land in Claude's context, so the session is built on the assumption that a PR may try to steer it.

Claude gets Read, Grep, Glob and gated Bash only — no Write or Edit. Every tool call is checked by a PreToolUse hook, which is the one place a check reliably holds: naming a tool in allowed_tools auto-approves it before a can_use_tool callback runs, and the CLI auto-approves read-only tools like Read on its own regardless. The hook enforces two things:

  • Paths stay in the checkout. Read, Grep, Glob and every path in a Bash command must resolve inside the PR worktree. Symlinks are resolved first, so a link the PR adds is judged by where it really points, and .. and ~ are refused outright. Your ~/.aws/credentials, ~/.netrc and gh's own token file are out of reach.
  • Bash only reads. Commands pass an allowlist of executables and git subcommands, and a denylist of the arguments that turn one of those into something else — find -exec, sort -o, --output=, git --exec-path. Shell expansion ($VAR, $(…)), redirection and backgrounding are refused, so environment variables are not a way to read a credential either.

Every process Claude starts also runs with git and gh credentials stripped and GITHUB_TOKEN/GH_TOKEN blanked, so a session cannot write to GitHub. It runs with setting_sources=[], so the reviewed repository's own CLAUDE.md and your global settings don't reach it.

Two limits worth stating plainly. Everything Claude reads is sent to the Anthropic API as part of the conversation, so the guarantee is about what it can read, not about keeping what it reads local — treat the checkout itself as the boundary. And a command allowed by the gate can still write inside the worktree; the checkout is disposable and recreated per PR head, but "read-only" means "cannot reach past the checkout", not "cannot touch a byte".

The walkthrough is the one exception

A walkthrough has to write a file, so that run gets its own session — never the one answering your questions — with a policy that differs in exactly two ways. It may invoke one skill, dev-utils:explain-diff-html, loaded by path so setting_sources can stay empty and nothing else from your config or the reviewed repository follows it in. And Write and Edit are allowed for one path: the output file the app named before the run started, checked by equality rather than by directory, with a symlink at either end refused. Everything else is the read-only gate above, including Bash.

The page itself runs JavaScript that Claude wrote after a run spent reading an untrusted PR, so it is served under sandbox allow-scripts with default-src 'none'. That leaves the quiz working in an opaque origin with no way to reach these routes and no way to send anything anywhere.

Where it keeps things

One folder, ignored by git, inside the checkout you run from:

.pr-reviewer/
  memory.json                # the PRs you've opened, your questions, the answers
  repos/owner__repo/         # a blobless clone, shared by every PR in that repo
  worktrees/owner__repo__7/  # a detached worktree at that PR's head
  traces/owner__repo__7.json # that PR's call-trace index, keyed by head commit

memory.json is rewritten whenever a question is asked, an answer finishes, or a commit note lands, and read when the server starts — so restarting brings back the PRs you were reading, with their threads and the notes you already paid for. Follow-ups continue the same Claude conversation where the CLI still has the transcript, and re-establish themselves in a new one where it doesn't. A question that was mid-answer when the server stopped comes back marked interrupted.

The home page lists what the folder is holding, largest first, with the PRs loaded from each clone. Clear drops one PR — its worktree, its questions, its answers — and takes the clone with it unless another loaded PR is still using it. Clear repo takes the clone, its worktrees and every PR in it, and reaches clones no review points at any more. Clear everything empties repos/ and worktrees/. Clearing is not the same as closing: the questions and answers go, and re-opening the PR starts a new review. The clone comes back on its own next time you open a PR from that repository.

The folder is self-contained and disposable: nothing in it points outside itself, so it can be moved and the reviews move with it, and deleting it loses the history rather than the tool. It is written for your user only (memory.json is 0600 — it quotes diffs and answers from repositories that may be private). Set PR_REVIEWER_HOME to put it somewhere else.

Walkthroughs are the one thing written outside it, because a walkthrough is usually read once and then forgotten:

/tmp/pr-reviewer-1000/2026-08-13-walkthrough-acme-demo-7-a1b2c3d4.html

The uid is in the directory name and a symlink there is refused, since the temp directory is shared and what lands in it is code from a repository that may be private; the directory itself is 0700. The head commit is in the filename, so pushing new commits asks for a new walkthrough rather than reopening one written for code that has moved. Clearing a PR deletes every walkthrough it wrote, which is what keeps the folder above the only thing you have to delete. TMPDIR moves them.

Which default you get depends on how you started the server. uv run pr-reviewer run from inside the project directory runs the checkout in place, so .pr-reviewer/ lands there. uvx --from /path/to/pr_reviewer and uvx --from git+https://… both build the package into uv's own cache and run it from there instead of the checkout — with no checkout to sit in, they fall back to ~/.local/share/pr-reviewer.

One thing to know about it sitting inside the project: the worktrees hold code from the pull requests you review, which is not your code and is not trusted. Nothing here runs it — pytest doesn't collect from a dot-directory, and the build only packages src/ — but that is a property of the tools' defaults, not something this repo enforces. Don't aim anything at the folder that would run what it finds, and don't turn an agent loose on "the whole repo" while it's there. PR_REVIEWER_HOME moves it out of the tree if you'd rather.

Tests

uv run pytest

They run against a real git repository built in a temp directory and a fake gh; nothing hits the network and no Claude session is started.

About

Dev tool: A github-like PR reviewer supporting calls to your local claude code to answer context specific questions

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages