An autonomous AI code reviewer that reads your pull requests so humans don't have to read them twice.
Fetches a GitHub PR, reviews the diff and hunts for vulnerabilities in parallel, then posts one structured, actionable review comment back on the PR. Install it as a GitHub App, try it on any public PR from the website, and watch every review on a live dashboard.
๐ Live site ยท ๐ Dashboard
Architecture โข Sample Output โข Quick Start โข Web UI โข Deploy โข GitHub App โข Engineering Notes โข Testing
Code review is the highest-leverage quality gate in software โ and the first thing that
gets skipped when a team is shipping fast. A stale eval() on user input or a
string-concatenated SQL query slips through not because reviewers are careless, but
because they're reviewing their fourth PR before lunch.
CodeInspectorAI is a multi-agent reviewer that never gets tired, never skips the security pass, and always posts the same structured feedback โ in under a minute, automatically, on every push.
| Feature | What it means | |
|---|---|---|
| โก | Parallel agents | Code-quality review and security scanning run concurrently in the same LangGraph superstep โ roughly half the wall-clock of a sequential chain |
| ๐ก๏ธ | Defence in depth | A deterministic regex sweep for leaked credentials runs alongside the LLM, so an obvious secret is caught even if the model misses it |
| โ๏ธ | Deterministic verdicts | The LLM finds problems; code decides the outcome. A critical finding always means changes_requested โ never a function of the model's mood |
| ๐ฉน | Fail-soft by design | Any node can die without killing the run. Lose the security scan and you still get the review, flagged partial with the error disclosed |
| ๐ | Idempotent comments | Pushing 10 commits updates one comment in place instead of spamming ten |
| ๐ฏ | Smart diff budgeting | Lockfiles, minified bundles and binaries are stripped; what's omitted is named in the prompt so the model knows its view is partial |
| ๐ค | GitHub App | Anyone can install it on the repos they choose. Each review runs on a short-lived token scoped to that installation, and comments come from a [bot] account |
| ๐ฅ๏ธ | Live web demo | Paste any public PR URL and watch the agents run step by step, then see the findings, a preview of the PR comment and the agent timing |
| ๐ | Review dashboard | Verdicts, quality scores, security findings and review times across every PR, with charts and a searchable history |
| ๐ | Four entry points | GitHub App / webhook, website, REST API, or CLI โ same graph behind all of them |
| ๐ | Verified webhooks | HMAC-SHA256 signature checking, draft-PR filtering, background execution so GitHub never waits on an LLM |
Built as a stateful directed graph rather than a linear chain, so the two analysis agents genuinely run in parallel and the workflow can abort early without burning tokens.
flowchart TD
A["๐ GitHub App / Webhook<br/>ยท Website ยท REST API ยท CLI"] --> B["๐ฅ Fetch Context"]
B -->|"PR fetch failed"| Z["๐ END"]
B --> C["๐ Code Review Agent"]
B --> D["๐ก๏ธ Security Agent"]
C --> E["๐งฉ Summary"]
D --> E
E --> F["๐ฌ Post Review"]
F --> Z
style A fill:#1f2937,stroke:#4b5563,color:#f9fafb
style B fill:#1e3a8a,stroke:#3b82f6,color:#eff6ff
style C fill:#065f46,stroke:#10b981,color:#ecfdf5
style D fill:#7f1d1d,stroke:#ef4444,color:#fef2f2
style E fill:#78350f,stroke:#f59e0b,color:#fffbeb
style F fill:#4c1d95,stroke:#8b5cf6,color:#f5f3ff
style Z fill:#1f2937,stroke:#4b5563,color:#f9fafb
Why the abort edge matters: if the PR can't be fetched, a naive graph would still invoke both LLM agents on an empty diff and post a meaningless review. The conditional route short-circuits straight to
ENDโ zero wasted tokens.
Designing the state before writing the nodes kept each one's responsibility honest:
| Node | Consumes | Produces |
|---|---|---|
| Fetch Context | owner, repo_name, pr_number |
raw_diff, changed_files, commit_messages, pr_metadata, pr_intent, parsed_diff, file_summaries, language_detected |
| Code Review โ | parsed_diff, file_summaries, pr_intent |
review_comments, code_quality_score, suggestions |
| Security โ | parsed_diff, file_summaries, language_detected |
security_issues, severity_levels, vulnerable_lines |
| Summary | all findings | final_summary, overall_verdict, formatted_comment, key_recommendations |
| Post Review | formatted_comment |
posted_comment_url, review_status |
โ = runs concurrently
Given a diff that introduces four classic vulnerabilities, here's what actually lands on the PR:
๐ด Changes requested ย โขย Quality score 3.0/10 ย โขย 1 file reviewed ย โขย 4 security issues (2 high/critical)
This change adds a user-lookup helper, but the query is assembled by concatenating the raw
usernameinto SQL, which is directly exploitable. Therun_cmdhelper hands unsanitised input toos.system, and the database password is committed in plaintext.
Severity Location Issue Recommendation ๐ด Critical app/db.py:6SQL injection via string concatenation of user-supplied usernameUse a parameterised query: cur.execute("SELECT * FROM users WHERE name = ?", (username,))๐ด Critical app/db.py:10Command injection: user-controlled nameconcatenated intoos.systemUse subprocess.run([...])with an argument list and validate the host๐ High app/db.py:3Hardcoded database password in source Move to an environment variable and rotate the credential ๐ก Medium app/db.py:13Reflected XSS: user comment inserted into HTML unescaped Escape with html.escape()or use the template engine's autoescaping
- ๐ด Critical
app/db.py:6โ the query concatenates the rawusernamevalue into the SQL string
- Suggestion: switch to parameter binding
- ๐ก Warning
app/db.py:13โ thecommentstring is inserted directly into HTML without escaping
Generated by CodeInspectorAI โ an automated reviewer. Treat its findings as suggestions, not gospel.
And on a clean PR, it says so and gets out of the way โ โ
Approved, no invented nitpicks.
git clone https://github.com/Tharun0127/CodeInspectorAI.git
cd CodeInspectorAI
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS / Linux
pip install -r requirements.txt
copy .env.example .env # cp on macOS / LinuxAdd your keys to .env, then review any public PR without posting anything:
python -m app.cli pallets/flask 6136 --dry-run| Variable | Required | Default | Notes |
|---|---|---|---|
GROQ_API_KEY |
โ | โ | console.groq.com/keys |
GROQ_MODEL |
โ | openai/gpt-oss-120b |
Any Groq chat model |
GITHUB_TOKEN |
to post* | โ | Fine-grained PAT ยท Pull requests: read & write |
GITHUB_WEBHOOK_SECRET |
recommended | โ | Verifies X-Hub-Signature-256 (the App's webhook secret when using one) |
GITHUB_APP_ID |
for the App | โ | See GitHub App |
GITHUB_APP_SLUG |
for the App | โ | From https://github.com/apps/<slug>; powers the Install button |
GITHUB_APP_PRIVATE_KEY |
for the App | โ | The .pem key, or set GITHUB_APP_PRIVATE_KEY_PATH |
POST_COMMENTS |
โ | true |
false = run everything, post nothing |
MAX_DIFF_CHARS |
โ | 15000 |
Diff budget per LLM node |
DASHBOARD_TOKEN |
โ | โ | Unlocks private-repo reviews on the dashboard |
DEMO_ENABLED |
โ | true |
The website's "try it" box |
DEMO_RATE_LIMIT |
โ | 5 |
Demo reviews per visitor per hour |
REVIEW_CONCURRENCY |
โ | 2 |
Reviews that may run at the same time |
DATA_DIR |
โ | ./data |
Where review history (SQLite) is stored |
* Not needed on repos where the GitHub App is installed; the App's own token is used there.
โ ๏ธ Groq retires models regularly. If a run fails with a model error, list what your account can use:curl -H "Authorization: Bearer $GROQ_API_KEY" https://api.groq.com/openai/v1/models
python -m app.cli owner/repo 42 # review and post
python -m app.cli owner/repo 42 --dry-run # review, print, post nothing
python -m app.cli owner/repo 42 --save out.md # save the markdownuvicorn app.main:app --reload --port 8000Then open http://localhost:8000 for the website and http://localhost:8000/dashboard.
| Endpoint | Purpose |
|---|---|
GET / |
Landing page with the live "try it" demo |
GET /dashboard |
Review history, stats and charts |
GET /health |
Readiness + which credentials are configured |
POST /review |
Manual trigger โ {"owner", "repo_name", "pr_number"} |
POST /webhook |
GitHub webhook: pull requests, plus App install/uninstall events |
POST /api/reviews |
Start a demo review โ {"pr_url": "https://github.com/o/r/pull/1"} |
GET /api/reviews[/{id}] |
Recent reviews, or one review with live step progress |
GET /api/stats |
Numbers behind the dashboard |
GET /docs |
Interactive OpenAPI docs |
Point a repository webhook at https://<your-host>/webhook:
- Content type โ
application/json - Secret โ same value as
GITHUB_WEBHOOK_SECRET - Events โ Pull requests only
Reviews fire on opened, reopened, synchronize and ready_for_review. Drafts are
skipped. The endpoint returns immediately and the review runs in the background, so
GitHub's 10-second webhook timeout is never at risk.
The same server hosts the website. No separate frontend build: plain HTML, CSS and JS
in app/static/.
Landing page (/): paste a public PR URL and watch the run live. The five agent
nodes light up as they finish, a console logs each step with timestamps, and the result
shows the verdict, a quality-score ring, findings sorted by severity with suggested
fixes, a preview of the exact comment the bot would post, and a timing chart that
shows how much time running the two agents in parallel saved. Every result has a
shareable link (/?r=<id>).
Dashboard (/dashboard): totals, average quality score, findings and review time;
charts for reviews per day, verdict mix, score distribution and languages (each with a
table view); and a searchable, filterable history. Click any review to open it in full.
It refreshes every 10 seconds.
Safe to leave public:
- Demo reviews are dry runs. Nothing is posted, private repos are refused (so nobody
can use the server's token to read private code), and each visitor gets
DEMO_RATE_LIMITreviews per hour.DEMO_ENABLED=falseturns the demo off. - The dashboard only shows public-repo reviews until it's unlocked with
DASHBOARD_TOKEN, which is kept only in that browser. - History is stored in SQLite under
DATA_DIR(/app/datain the container).
The service ships as a Docker image, so it runs on any container host. It needs a long-running process: reviews run as background tasks after the webhook returns, so serverless platforms that freeze the function after the response (Vercel, Lambda) will silently drop them.
docker build -t codeinspector-ai .
docker run -d -p 8000:8000 --env-file .env codeinspector-ai
curl localhost:8000/healthThe container listens on $PORT (default 8000), runs as a non-root user and has a
built-in HEALTHCHECK against /health.
render.yaml is a Render Blueprint: click the button above (or New โ Blueprint in the
Render dashboard and pick this repo) and fill in GROQ_API_KEY and GITHUB_TOKEN when
prompted. Leave the GITHUB_APP_* values empty until you've registered the
GitHub App. GITHUB_WEBHOOK_SECRET and
DASHBOARD_TOKEN are generated for you; find them in the service's Environment tab.
Render's free plan sleeps after ~15 minutes idle. The first request after a sleep takes about a minute, and can exceed GitHub's 10-second webhook timeout, so GitHub marks that delivery as failed even though the review still runs. Use a paid instance for reliable delivery.
The free plan also has no persistent disk, so dashboard history resets on each deploy or restart. On a paid plan, attach a disk mounted at
/app/datato keep it.
All of these detect the Dockerfile automatically. Set the same environment variables
as in .env.example, expose the port the platform assigns via $PORT, and use /health
as the health check path.
Vercel can serve the pages, but it can't run reviews dependably:
- Reviews run in the background after the request returns, and serverless functions are frozen at that point.
- The live demo polls the server that is running the review, but serverless requests can land on a different instance that knows nothing about it.
- The filesystem is temporary, so dashboard history doesn't survive.
To use a Vercel domain, run the app on Render (or any host above) and have Vercel forward every request to it with a rewrite.
prompts/ is git-ignored, so deploys built from the repo use the built-in fallback
prompts. To use your own, either build the image locally (a local prompts/ folder is
copied in), or mount the files and set PROMPTS_DIR:
docker run -d -p 8000:8000 --env-file .env \
-v "$PWD/prompts:/prompts:ro" -e PROMPTS_DIR=/prompts codeinspector-aiRun as a GitHub App, CodeInspectorAI works like any app from the Marketplace:
- A developer clicks Install, picks which repos it may see, and approves.
- GitHub then sends the app every pull request event on those repos. Nobody sets up a webhook.
- For each review, the server swaps the app's private key for an installation token that only works on that installation's repos and expires within an hour. Nobody shares a password or personal token.
- Comments appear from
<your-app>[bot]. - Uninstalling (GitHub โ Settings โ Applications) cuts access immediately, and the server forgets any cached token for that installation.
-
Deploy the server first (see Deploy) so you have its URL.
-
Go to GitHub โ Settings โ Developer settings โ GitHub Apps โ New GitHub App (or under your organisation's settings to own it as an org) and fill in:
Field Value GitHub App name e.g. CodeInspectorAI(its URL slug becomesGITHUB_APP_SLUG)Homepage URL https://<your-host>/Setup URL https://<your-host>/(tick Redirect on update)Webhook URL https://<your-host>/webhook(Active ticked)Webhook secret a long random string, the same value as GITHUB_WEBHOOK_SECRETRepository permissions Pull requests: Read & write ยท Contents: Read-only ยท Metadata: Read-only (automatic) Subscribe to events Pull request Where can it be installed? Any account (or Only this account while testing) -
Create it, then on the app's page note the App ID and click Generate a private key (downloads a
.pemfile). -
Set these on the server and redeploy:
GITHUB_APP_ID=123456 GITHUB_APP_SLUG=codeinspectorai # from https://github.com/apps/<slug> GITHUB_APP_PRIVATE_KEY=<contents of the .pem file> GITHUB_WEBHOOK_SECRET=<the webhook secret from step 2>
For the key, open the
.pemfile in a text editor and paste the whole thing,-----BEGINand-----ENDlines included. Real line breaks work, and so does one line with a literal\nbetween lines. Or upload the file (e.g. as a Render Secret File) and setGITHUB_APP_PRIVATE_KEY_PATH=/etc/secrets/<file>.peminstead.Never commit the
.pemfile: anyone holding it can act as your app. -
Check
GET /healthshows"github_app_configured": true. The landing page now shows Install on GitHub, linking tohttps://github.com/apps/<slug>/installations/new.
GITHUB_TOKEN becomes optional: it's only used for repos where the app isn't installed,
for example the public "try it" demo. Setting one raises GitHub's rate limit for those
reads from 60 to 5,000 per hour.
The interesting decisions weren't about prompting โ they were about what happens when things go wrong.
โ๏ธ The LLM finds problems; code decides the verdict
An LLM asked to both find issues and rule on them will happily approve a PR it just
told you contains a critical vulnerability. So decide_verdict() in graph/utils.py
owns the outcome:
if any(issue.severity in ("high", "critical") for issue in security_issues):
return "changes_requested"Same findings in, same verdict out โ every time. The model's job is evidence, not judgement.
๐ฉน Fail-soft nodes: partial results beat no results
Every node catches its own exceptions and appends to a reducer-backed errors list
instead of raising. If the security scan dies, the review still posts โ with the failure
disclosed in a collapsible block and review_status downgraded to partial.
The one exception is a failed PR fetch, which routes straight to END: there is nothing
to review, so spending two LLM calls to find that out would be waste.
๐ง Surviving flaky structured output
Open-weight models on Groq intermittently emit an invalid tool call
(400 โ attempted to call tool 'json'), which killed an entire agent mid-run. This
surfaced during live testing, not code review.
app/llm.invoke_structured() now degrades gracefully: tool calling โ retry โ JSON mode
with the schema rendered into the prompt. Severity fields are normalised too, so a
model answering "high" where the schema wants "critical" gets mapped instead of
throwing away an otherwise valid finding.
๐ก๏ธ Regex and LLM, not regex or LLM
Credential patterns are matched deterministically against added lines only โ so pre-existing secrets aren't re-reported on every push โ then merged with the LLM's findings and deduplicated.
The regexes are tested against real-world shapes (DB_PASSWORD, apiKey, AWS keys, PEM
blocks) and against false positives like password = os.environ["DB_PASSWORD"], which
must not fire.
๐ Honest context budgeting
Rather than silently truncating a diff at 15,000 characters and letting the model review
a fragment while believing it saw everything, build_diff_context() packs whole files
and appends an explicit manifest:
--- NOT SHOWN IN THIS DIFF ---
- vendor/bundle.js (too large for the context budget)
The model knows what it doesn't know.
๐ One comment, not twenty
Every posted review carries an invisible <!-- codeinspector-ai-review --> marker. On
the next push the bot finds its own previous comment and edits it in place, so a
20-commit branch has exactly one always-current review instead of a wall of noise.
If that comment can't be edited (someone else's comment carrying the marker, or one
left under a different identity), it posts a fresh one instead of failing.
๐ค One token per review, never shared between them
As a GitHub App, every review must act as its installation, and two reviews for two
different users can run at the same time. So the token isn't a global: the runner
mints the installation's token, then runs the graph inside use_token(...), which sets
a ContextVar. Every GitHub call (fetching the PR, the diff, posting the comment) reads
it from there, and it's reset when the review ends. A test asserts that both the fetch
and the post saw the installation's token, and that nothing leaked afterwards.
Tokens are cached per installation and refreshed five minutes before they expire, and an uninstall event drops the cached token immediately.
Live-testing the App against the real library turned up a nasty one: PyJWT โฅ 2.10
rejects a numeric iss claim, and PyGithub passes the App ID through as-is. Every token
request would have failed in production while every mocked test passed. The app ID is
now passed as a string, and a regression test signs a real RSA key and decodes the JWT.
python -m pytest # 144 tests ยท ~4s ยท zero network calls| Suite | Covers |
|---|---|
test_utils.py |
Diff parsing, language detection, verdict rules, budgeting |
test_nodes.py |
Each node's success and failure path, secret regexes |
test_graph.py |
Compiled graph end-to-end, parallel fan-out, abort route |
test_llm.py |
Structured-output retry/fallback, severity normalisation |
test_api.py |
Endpoints, webhook filtering, HMAC signature verification |
test_github.py |
Diff fallback, metadata extraction, comment replacement |
test_github_app.py |
Installation tokens & caching, per-review identity, install events, JWT signing |
test_web.py |
Review store, live progress, demo guards & rate limit, dashboard privacy, pages |
GitHub and the LLM are both faked, so the suite is fully offline, deterministic and free to run โ it needs no API keys and burns no tokens. Failure paths are tested as carefully as happy paths, because a reviewer that crashes on a flaky API is a reviewer nobody trusts.
โโโ app/
โ โโโ config.py Environment/settings, prompt loading
โ โโโ llm.py Groq factory + resilient structured-output helper
โ โโโ github.py PR fetch, diff, posting, webhook signatures, GitHub App tokens
โ โโโ main.py FastAPI app (webhook + REST trigger)
โ โโโ web.py Website, dashboard and their JSON API
โ โโโ runner.py Runs a review with live step tracking, as the right identity
โ โโโ store.py SQLite review history + installations
โ โโโ cli.py Command line entry point
โ โโโ static/ Landing page + dashboard (plain HTML/CSS/JS, no build step)
โโโ graph/
โ โโโ state.py ReviewState + Pydantic output schemas
โ โโโ utils.py Diff parsing, language detection, verdict rules
โ โโโ graph.py LangGraph wiring
โ โโโ nodes/ context ยท review ยท security ยท summary ยท post_review
โโโ tests/ 144 offline tests
โโโ Dockerfile Production image (non-root, health check)
โโโ render.yaml One-click Render deploy
Note: the system prompts in
prompts/*.mdare loaded at runtime and are not published in this repository. Each node falls back to a built-in default prompt when the file is absent, so the project runs and the full test suite passes without them.
| Layer | Choice | Why |
|---|---|---|
| Orchestration | LangGraph | Stateful graphs give real parallelism and conditional routing, which a linear chain can't |
| Inference | Groq ยท gpt-oss-120b |
Sub-second latency keeps a full review under a minute |
| Validation | Pydantic v2 | Schema-bound LLM output โ no fragile regex parsing of prose |
| API | FastAPI | Async, background tasks, free OpenAPI docs |
| Integration | PyGithub + REST | PyGithub for objects, raw REST for the diff media type it doesn't expose |
| Identity | GitHub App | Per-install, per-repo, hour-long tokens instead of one shared personal token |
| Storage | SQLite | Zero-ops review history; one file on a disk |
| Frontend | Plain HTML/CSS/JS | Served by FastAPI itself: one deploy, no build step |
- Inline review comments anchored to diff positions (not just a summary comment)
- Repository-aware context via embeddings, so the reviewer sees beyond the diff
- Configurable per-repo rules through a
.codeinspector.yml - Review-quality feedback loop from ๐/๐ reactions on posted comments
- Dockerfile + one-click deploy
- Website with a live demo + review dashboard
- GitHub App: install on any account, scoped short-lived tokens,
[bot]comments
Built by Tharun
If this was useful, a โญ is appreciated.