A tool for reviewing code you're accountable for but didn't write by hand — typically an agent's implementation of a plan you agreed. It uses an LLM to direct your attention rather than to gate: it clusters the diff into semantic groups and annotates what each block changed, so you can judge whether the change matches the intent and how it was built. Nothing is posted or applied without you.
The same review runs at two radii. As a Claude Code plugin
(/scr:review), your inline comments are drafts until you send them;
each Send reaches the authoring agent as it happens, and its answers
appear in the thread. As scr pr, the
same flow runs against a GitHub PR: each comment you send goes into
your pending review on GitHub, and Submit publishes it as one review
the author — or their agent — picks up later. Both open one
browser viewer: a side-by-side diff with semantic-group navigation,
fold-level "what did this block change" annotations, and inline comments
you leave by clicking a line number.
Two more affordances inside that viewer:
- Review console — an in-viewer chat that answers questions about the change ("what calls this?", "why is this guard here?", "draw the control flow") against the same worktrees, tools, and annotations the augment pass used, so you never drop to a separate Claude session that lacks the run's context.
- Rendered-markdown mode — a per-file toggle on
.mdfiles that swaps the line-grid for a two-pane base→head rendered view (GFM, math, mermaid), block-level delta and folding, so you can judge whether the finished prose reads well rather than only which lines moved. The text diff stays authoritative for hunks, spans, and commenting.
For a screenshot tour of the viewer — the sidebar axes, the fold ladder, spans, comments, and the review console — see docs/walkthrough.md.
Prerequisites on the user's machine:
- Python 3.11 or newer on
PATH gitonPATH- Either
ANTHROPIC_API_KEYin the environment (or a.envin the repo you run/scr:reviewfrom), or a logged-inclaudeCLI onPATHfor OAuth-based fallback (no API key required)
Optional:
gh— only needed for GitHub-PR review (scr pr)ripgrep(rg) — speeds up the LLM's code-search tool;git grepis used as a fallback
Install:
/plugin marketplace add populationgenomics/semantic-code-review
/plugin install scr
The first time you run /scr:review, the plugin's bin/scr wrapper
creates a Python virtualenv under $CLAUDE_PLUGIN_DATA/venv, installs
the dependency closure with pip install --require-hashes (so any
tampered tarball fails the install instead of silently landing), then
installs semantic-code-review from PyPI at the version pinned in the
plugin's pyproject.toml. That published wheel carries the prebuilt
viewer.js bundle as package data, so the wrapper needs no Node, npm, or
build step — the git checkout is a thin launcher over a PyPI release. The
install is stamped (lockfile hash + version) and only re-runs when one
changes; subsequent invocations exec the cached venv and start
immediately.
/scr:review HEAD~1 # diff working tree vs one commit back
/scr:review main..HEAD # committed-only diff of the current branch
/scr:review HEAD --spec SPEC.md # with a spec markdown as LLM ground truth
The command opens a browser viewer. The left sidebar lists semantic groups (LLM-curated clusters of related hunks); click one to filter the visible hunks to that group, click "Show all" to clear. Hunks that no group claimed get a subtle dotted-border tell so you can spot them at a glance.
Leave inline comments by clicking a line number on either side of the diff. A comment is a draft — yours, editable, never lost — until you Send it: one comment with its Send button, or every draft with Send all drafts. What you send reaches Claude Code as a batch as soon as you send it; one comment sent alone means "look at this now". Claude answers into the thread and can resolve it; your follow-up is a reply you Send. The bar shows whether Claude is listening. There is no Done: the review ends when the tab has been closed for the idle period, and anything you never sent reaches Claude then as the final list.
Under the hood, scr review returns at once with a run id and leaves a
detached server running; scr review --wait <run_id> is how Claude
listens, and scr comment reply / resolve how it answers. See
commands/review.md for the full slash-command prompt.
scr picks a backend automatically:
ANTHROPIC_API_KEYset → uses the Anthropic SDK directly (best performance: prompt caching, native concurrency, structured tool use).- Otherwise,
claudeonPATH→ falls back toclaude -psubprocesses that connect to one warm HTTP MCP serverscrhosts, exposing the same repo-tools the SDK path uses. No API key needed; works with any logged-in Claude Code installation. - Neither available → fails fast with a clear error.
Force a backend with --backend <name>. Run scr config show for
the registered names — builtins plus anything you've added under
[backends.<name>] in your config.
The Gemini backend is opt-in only — never picked by auto.
--backend=gemini-apiuses Google's officialgoogle-genaiSDK directly. Auth ladder:GOOGLE_CLOUD_PROJECTset → Vertex AI via Application Default Credentials; elseGEMINI_API_KEY/GOOGLE_API_KEY→ AI Studio. Native concurrency, full structured tool use, Gemini's implicit prompt caching surfaces ascache_read_input_tokensin our usage stats. Best Gemini path for production work.
scr ships builtin profiles for several free-tier providers. None
require an Anthropic or Google paid plan. Pick one, set the listed
env var, pass --backend <name>:
--backend |
Auth | Notes |
|---|---|---|
gemini-api |
GEMINI_API_KEY from aistudio.google.com |
Free tier with rate limits; hits the Google SDK directly. |
groq |
GROQ_API_KEY from console.groq.com |
Free tier, very fast inference, Llama 3.3 70B by default. |
github |
GITHUB_TOKEN (any GitHub account) |
Free quota across multiple model families; default is openai/gpt-4o-mini. |
cerebras |
CEREBRAS_API_KEY from cloud.cerebras.ai |
Free tier; pass --model (catalogue rotates). |
openrouter |
OPENROUTER_API_KEY from openrouter.ai |
Mixed paid + free models; pass --model (e.g. meta-llama/llama-3.3-70b-instruct:free). |
mistral |
MISTRAL_API_KEY from console.mistral.ai |
La Plateforme free tier; default is codestral-latest. |
ollama |
none — runs against localhost:11434 |
Local llama.cpp/Ollama; pass --model to name something you've pulled (e.g. qwen2.5-coder:14b). |
The non-Anthropic / non-Google entries all reach the provider via
the OpenAI Chat Completions wire format (type = "openai-compat").
Override any builtin's model — or add a new provider — with a
[backends.<name>] block in your config; see scr config edit and
docs/config.md for the full config structure.
If your bearer lives in a secret store rather than a long-lived env
var, use api_key_command: it runs a shell-free argv command and
reads the bearer from stdout. Built-in example: --backend=github
falls back to gh auth token when GITHUB_TOKEN isn't set. Custom
example, Anthropic key in a GCP Secret Manager entry:
[backends.claude-api]
api_key_command = ["gcloud", "secrets", "versions", "access",
"latest", "--secret=anthropic-api-key"]The same hook works on claude-api, gemini-api, and any
openai-compat backend. For gemini-api, GOOGLE_CLOUD_PROJECT
takes precedence: when set, the Vertex/ADC path wins and
api_key_command is skipped.
Quality caveat: any non-frontier backend produces shallower hunk
intents and more spurious refs[]. The output is still useful as a
draft you skim, especially for small PRs, but it isn't a Claude /
Gemini Pro replacement.
Install from PyPI. The published wheel already contains the compiled viewer bundle, so no Node is needed at runtime:
uv tool install semantic-code-review
# or: pipx install semantic-code-review / pip install semantic-code-review
scr init # interactive first-run setup
scr review HEAD~1..HEAD --spec SPEC.md
scr init is the quickest way to get configured: it detects which
backends are usable in your environment, lets you pick a default backend
and model (live-listed from the backend once a credential resolves), and
guides credential setup — offering, per backend, to set an env var, fetch
the key from a command (api_key_command), write it to a gitignored
.env, or (user scope only) store it in your user config's [env]. All
config and secret files are written 0600.
From a source checkout (for development, or to pin the exact hashed dependency set), build the viewer bundle yourself:
pip install --require-hashes -r requirements.lock
pip install --no-deps --no-build-isolation .
npm ci --ignore-scripts && npm run build
scr review HEAD~1..HEAD --spec SPEC.md
The three commands you'll actually use:
scr init— interactive setup: detect usable backends, pick a default + model, guide credential setup, write the config. Run once.scr review <ref-or-range> [--spec SPEC.md]— review a local git diff. Starts a detached server that runs the LLM augment pass and opens the viewer, then returns at once withrun_id: <slug>. Also takes a second endpoint for an explicit two-sided diff — two refs (scr review e4e8f74 HEAD, whole-tree) or tworev:pathblobs (scr review A:old.py B:new.py, single-file; cross-path shows as a rename).scr review --wait <run_id>— block (540 s by default,--wait-timeout) for the next batch of comments the reviewer sends; printsstatus: batchand the batch as markdown,status: nothing-yet, orstatus: endedwith the comments never sent. Exit 0 for all three, 2 for an unknown run id.scr comment reply <run_id> <comment_id> [BODY],scr comment resolve|unresolve <run_id> <comment_id>— answer into a live review's thread, or settle it.scr pr <owner/repo> [<number>]— the same flow against a GitHub PR. Omit the number to pick from the open PRs requesting your review. Returns at once with the viewer's URL and the run id, likescr review; the server keeps running until the tab has been closed for the idle period. Needs theghCLI onPATHand authenticated.
scr pr is scr review with GitHub as the counterpart: same fetch,
augment, viewer and comment store, but a comment you Send goes into
your pending review on GitHub — created on the first Send, or resumed
if you already have one on the PR, whose comments then show as
pending — and Submit in the bar publishes it as Comment, Approve
or Request changes with an optional summary. Approve with no comments
is the LGTM. Editing a pending comment updates it on GitHub in place;
deleting one deletes it there. A Send GitHub refuses leaves the comment
unsent, retried on the next Send and every 30 seconds while the tab
is open, and Submit refuses until every comment has landed. Resolving
or unresolving an existing review thread from the viewer flips it on
GitHub at once. Claude is not in the loop: nothing is printed for it
and there is no --wait.
On a PR that already carries review comments, scr pr ingests them into
the viewer alongside your own. Each ingested comment is re-anchored from
the commit it was written against to the PR's current head by diff
propagation, so it stays pinned to the code it addressed rather than being
collapsed as "outdated" the way GitHub's own UI does; the viewer tags each
as anchored, shifted, orphaned, or gone, and carries thread
resolution state across. Across successive revisions this keeps a review's
prior judgments attached to the code as it converges, instead of losing
them on every push.
Plumbing — side commands, for scripting or debugging
scr strip <augmented.diff>— strip annotations back to a plain unified diff on stdout.scr lint <augmented.diff>— validate the augmented-diff format.scr runs path— print the runs root resolved for the current cwd.scr config show | edit | path— inspect or edit the config files.
scr review and scr pr leave a review server running per run, and
the CLI owns those processes:
scr runs ps [--here] [--prune]— every recorded server, one line each: run id, state, URL, pid, uptime, version/build, counterpart, whether a--waitis listening, how many tabs are open. All repos' runs roots by default;--herefor the current repo's. A server whose process is gone shows asstale(removed with--prune); one running a different build of scr than the CLI shows asother-build.scr runs stop <run_id> | --all— SIGTERM, then SIGKILL if it has not gone within a few seconds. Exit 0 when a server was stopped, 1 when none was running.scr runs restart <run_id>— stop it and start it again from the installed build, with the arguments and working directory it was started with. If the viewer misbehaves after upgrading scr, this is the fix.scr runs logs <run_id> [-f]— the server's log;-ffollows until it exits.scr review … --foreground/scr pr … --foreground— serve in the invoking process instead: logs to stderr, blocks until Ctrl-C or the idle timeout.viewer:andrun_id:are still printed first, and--wait/scr commentreach it the same way.
A second scr review or scr pr for a run a server already holds
reuses that server only if it is the same build — the same installed
package, interpreter and viewer bundle. A server of another build (two
checkouts at one version, a re-installed venv, a rebuilt bundle) is
stopped and a fresh one started, with a line on stderr saying so; a
stale record is dropped.
scr writes per-review state — a meta.json, the raw and augmented
diffs, and base/ / head/ worktrees so the LLM (and the viewer)
can read pre- and post-change files — to a per-repo directory under
your XDG cache:
~/.cache/scr/runs/<sha256-of-git-common-dir>/<run-slug>/
Worktrees of the same repo share the directory; different repos get
different ones. Runs live outside the repo on purpose — a .scr/ at
the repo root is a deploy-tool footgun (gcloud, docker, tar tend to
upload it unless every project remembers to ignore it), and the
worktrees inside contain real git history that no one wants to
upload by accident.
Override with --runs-root <path> on any command that creates runs
(review, pr, fetch).
python3 -m venv .venv
.venv/bin/pip install --require-hashes -r build-requirements.lock
.venv/bin/pip install --require-hashes -r requirements-dev.lock
.venv/bin/pip install --no-deps --no-build-isolation -e .
npm ci --ignore-scripts
npm run build # tsc --noEmit typecheck, then esbuild bundle → semantic_code_review/viewer/assets/viewer.js
npm run test:js # vitest
.venv/bin/python -m pytest
tests/conftest.py autobuilds the TypeScript module if it's missing
when pytest starts, so python -m pytest Just Works on a fresh
checkout (provided Node is available).
Releases publish to PyPI from GitHub Actions via Trusted Publishing (OIDC) — no API tokens, no cloud credentials. To cut a release:
- Bump
versioninpyproject.toml(and.claude-plugin/plugin.jsonto match). - Commit, then publish a GitHub Release whose tag is
vX.Y.Zmatching that version..github/workflows/release.ymlfires on the published release: it compiles the viewer bundle, builds the wheel + sdist withuv build, and uploads to PyPI. A guard fails the run loudly if the tag andpyproject.tomlversion disagree.
The plugin installs semantic-code-review==<pyproject version> from
PyPI, so the version on the default branch must always be one that is
published (or being published) there — bump it only as part of cutting a
release. A plugin update that lands a version PyPI doesn't yet carry will
fail its first-run install until the release finishes.
One-time setup (recorded here for a fresh fork): register a
Trusted Publisher on the
PyPI project pointing at this repository, workflow filename
release.yml, and environment pypi; and create a pypi environment
in the repo's GitHub settings.
semantic_code_review/augment/— overview + per-hunk LLM pipeline, prompts, schemas, MCP tool wrapper.semantic_code_review/viewer/— Python side:build_json.py,hunk_layout.py. Frontend assets inviewer/assets/: the TypeScript modules (bootentry, plusrender,sidebar,folds,annotations,comments,console,sse, the rendered-markdown moderendered/markdown/katex/mermaid, …) bundled by esbuild into a singleviewer.js, alongsideviewer.css, the staticindex.htmlserved by the review server, and vendored assets (highlight.js,mermaid,KaTeX) underassets/vendor/.semantic_code_review/structural/— deterministic tree-sitter layer:parse.py(grammars + tag queries → aSymboltree),symbols.py, anddiff.py(base→headSymbolDelta). Feeds the Symbols sidebar axis, AST fold regions, and the overview seed.semantic_code_review/review/— local HTTP server that back-channels reviewer comments to the calling process, the sharedserve_reviewhelper used by bothscr reviewandscr pr, andgithub.pyfor the GitHub-PR round-trip viagh api.commands/review.md— the Claude Code slash-command prompt.bin/scr— bootstrap wrapper; preflights deps, maintains the Python venv, installsscrfrom PyPI at the pinned version, execs the realscr.TREE_SITTER.md— design history of the structural (tree-sitter) layer. Symbol grouping and AST fold regions are shipped; semantic hunk splitting is still future.
Every external dependency is pinned by exact version and SHA-256
hash. The bin/scr bootstrap installs with
pip install --require-hashes and npm ci, so any on-disk artifact
whose hash doesn't match the lockfile fails the install rather than
silently landing.
Lockfiles (all committed):
build-requirements.lock— PEP 517 build backend (setuptools,wheel).requirements.lock— runtime Python deps (anthropic,pydantic,typer, …).requirements-dev.lock— runtime +pytest+pytest-asyncio.package-lock.json— Node toolchain (typescript,vitest,@playwright/test,jsdom,@types/node).uv.lock— the uv-resolved lockfileuv build(the release workflow) anduv runresolve against; kept in sync with therequirements*.lockpip closure.
Frontend assets are vendored under
semantic_code_review/viewer/assets/vendor/ at pinned upstream
versions with each project's LICENSE file alongside — never loaded
from a CDN at runtime:
highlight.js+ light/dark stylesheets — inlined into the viewer HTML.mermaid(review console diagrams) andKaTeX+ its woff2 fonts (rendered-markdown math) — lazy-loaded by<script>/<link>injection from the vendored files the first time they're needed, so neither weighs on the baseviewer.jsbundle.
See vendor/VENDOR.md for provenance and hashes; vendor/refresh.sh
re-downloads at the pinned versions and fails loudly if any SHA-256
doesn't match.
Refreshing:
# Python
uv pip compile pyproject.toml -o requirements.lock --generate-hashes
uv pip compile pyproject.toml --extra dev -o requirements-dev.lock --generate-hashes
printf 'setuptools>=68\nwheel\n' | uv pip compile - --generate-hashes -o build-requirements.lock
# Node toolchain
npm install # commits an updated package-lock.json with new integrity hashes
# Frontend (edit version constants in refresh.sh first if bumping)
./semantic_code_review/viewer/assets/vendor/refresh.sh