tools/pr-approval-agent/README.md
AI-assisted PR approval for PostHog. Deterministic safety gates first, then Claude reviews for showstoppers.
Add the stamphog label to a non-draft PR.
The GitHub Action runs the agent and posts an approval or comment.
On approval the label stays so it's visible which PRs were stamphog'd.
On failure the label is removed so it can be re-applied.
# run from anywhere inside the posthog repo
uv run tools/pr-approval-agent/review_pr.py 46594
# dry run (gates only, no LLM calls)
uv run tools/pr-approval-agent/review_pr.py 46594 --dry-run
# save full result as JSON
uv run tools/pr-approval-agent/review_pr.py 46594 --output-json /tmp/review.json
# verbose (show agent tool calls)
uv run tools/pr-approval-agent/review_pr.py 46594 -v
Requires gh CLI authenticated and ANTHROPIC_API_KEY in your environment.
Uses PEP 723 inline metadata so uv run handles dependencies automatically.
"stamphog" label added to PR
│
▼
Prerequisites (hard gate)
- Not draft, no merge conflicts
- No outstanding "changes requested" reviews
│
▼
Deny-list (hard gate)
- Checks file paths + PR title against sensitive categories
- Any match → gates DENY
│
▼
Size ceiling (hard gate)
- >500 lines or >20 files → too large for auto-review
│
▼
Tier classification
- T0-deterministic: docs/tests/config only
- T1-agent: eligible for review (sub-classified by risk)
- T2-never: caught by deny-list
│
▼
LLM Review
- Claude Agent SDK with Read/Grep/Glob tools
- Explores the repo via git diff, reads source files if needed
- Looks for showstoppers: production breakage, security, missed deps
- Gates are authoritative — LLM can tighten but never loosen
│
▼
Final verdict → GitHub review (approve or comment)
The bot never posts request-changes — only approves or comments.
Lowest risk. LLM still reviews but with a lighter bar. PR touches only safe paths:
.md, .mdx, .txt, .rst, .json, .yaml, .yml, .toml, .ini, .cfg, .csv, .svg, .png, .jpg, .jpeg, .gif, .ico, .webp, .snap, .lockdocs/, README, CHANGELOG, LICENSE, CONTRIBUTING, .github/CODEOWNERS, .gitignore, .editorconfig, generated/, __snapshots__/Sub-classified by risk to calibrate scrutiny:
| Sub-tier | Lines | Files | Breadth |
|---|---|---|---|
| T1a-trivial | ≤20 | ≤3 | single-area |
| T1b-small | ≤100 | ≤5 | not cross-cutting |
| T1c-medium | ≤300 | ≤15 | not cross-cutting |
| T1d-complex | >300 or >15 | — | any |
Deny-listed categories where even a small diff can have high blast radius:
| Category | Patterns |
|---|---|
| auth | auth, login, signup, session, token, oauth, saml, sso, permission, oidc, credential, etc. |
| crypto_secrets | crypto, encrypt, decrypt, secret, key, cert, signing, .env, vault |
| migrations | migrations/, migrate, backfill, schema_change |
| infra_cicd | terraform, k8s, helm, dockerfile, .github/workflows, deploy, iam, cloudflare, etc. |
| billing | billing, payment, stripe, invoice, subscription, pricing |
| public_api | openapi, api_schema, swagger, public_api |
| deps_toolchain | package.json, requirements.txt, pyproject.toml, pnpm-lock, uv.lock, Cargo.toml, go.mod, etc. |
Uses .github/CODEOWNERS-soft as context for the LLM (not a hard gate).
Cross-team typo/test/comment fixes are fine; behavioral changes to business logic get escalated.
Every run produces a JSON evidence bundle (--output-json locally, uploaded as artifact in CI) containing:
The GitHub Action uploads this as a build artifact with 30-day retention.
review_pr.py — pipeline orchestrator (fetch → classify → gates → LLM)gates.py — deterministic classification and deny-list logicgithub.py — GitHub data fetching via gh CLIreviewer.py — Claude Agent SDK reviewer (showstoppers prompt).github/workflows/pr-approval-agent.yml — GitHub Action (label trigger)Tier thresholds and deny categories calibrated against 356 PRs that received quick human approval (stamp) in the PostHog repo over ~90 days:
fix, 101 chore — fixes and chores are the modal commit typesKey insight: size alone is not a safe proxy. Small PRs touching CI workflows, auth, or SAML should never be auto-approved regardless of size. The deny-list exists precisely for this.