prowl-review
v0.3.0
Published
BYOK AI code review for pull requests — summary, inline comments, and chat, powered by your own LLM key (Claude/OpenAI/Gemini).
Maintainers
Readme
prowl-review
BYOK (bring-your-own-key) AI code review for pull requests — the code-review pillar of the Prowl QA suite.
prowl-review reviews pull requests (summary + inline comments + @prowl-review chat) using your own LLM key — Claude (default), OpenAI, or Gemini — with no usage caps imposed by us. Because your key pays the provider directly, there's nothing to rate-limit: the only ceiling is your provider's own limits, which dwarf the per-hour caps of commercial reviewers.
It's delivered as a GitHub Action + local CLI (zero hosting), and is built to match — not just approximate — the quality of CodeRabbit/Greptile via agentic cross-file context, multi-pass specialized review, linter/SAST grounding, and false-positive verification.
Status: early development. This package currently contains the project scaffold and CLI surface. See
docs/backlog.mdfor the roadmap andCLAUDE.mdfor the design principles.
Documentation
Full documentation lives at review.prowl.tools — getting started, configuration, CLI + bot commands, the ensemble, grounding, cross-file context, and repo-wide learnings. Part of the Prowl QA suite.
Key reference pages also kept in this repo:
- Authentication & keys — BYOK setup, provider keys, token scopes
- Privacy & data handling — what leaves your machine and what doesn't
- Example review — a rendered sample walkthrough
- Releasing — npm + Homebrew release process
Usage (GitHub Action)
Add a workflow that runs the review on pull requests. For single-provider
reviews, store your provider key as the PROWL_AI_KEY repository secret.
# .github/workflows/prowl-review.yml
name: prowl-review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Serialize reviews and bot commands for the same PR so command side effects are
# not interrupted when new commits arrive. prowl-review re-checks the PR head
# before publishing and skips if it advanced, so stale reviews do not post.
concurrency:
group: prowl-review-${{ github.event.pull_request.number }}
cancel-in-progress: false
permissions:
pull-requests: write # post the review + inline comments
issues: write # create/update the summary comment
checks: write # optional merge-gate check run
contents: read
jobs:
review:
# Forks don't receive provider secrets. Draft handling happens inside
# prowl-review so review.reviewDrafts can opt into draft auto-reviews.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Optional: load Action config from the trusted base branch, not from PR code.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
path: prowl-review-config
persist-credentials: false
- uses: prowl-tools/prowl-code-review@v1
with:
ai-key: ${{ secrets.PROWL_AI_KEY }}
# config-path: prowl-review-config/.prowl-review.yml
# ai-provider: anthropic # optional anthropic | openai | gemini override
# ai-model: claude-... # optional per-provider model overrideFor ensemble reviews with provider-specific keys, omit ai-key and pass
PROWL_AI_KEY_ANTHROPIC, PROWL_AI_KEY_OPENAI, or PROWL_AI_KEY_GEMINI as
step env vars instead.
The concurrency block is the recommended pattern: keying the group to the PR
number serializes auto reviews with bot commands. cancel-in-progress: false
keeps in-progress maintainer-requested side effects such as pause, resume,
and break glass from being interrupted; stale auto reviews skip publishing
when the PR head has advanced.
Draft PRs & on-demand review (#28)
By default prowl-review skips draft PRs and reviews automatically once a PR
is marked ready for review (keep ready_for_review in the workflow's
on.pull_request.types). An explicit @prowl-review review reviews a draft
on demand. Two .prowl-review.yml keys tune this:
review:
reviewDrafts: true # also auto-review drafts (default false)
auto: false # on-demand only: review just when asked with @prowl-review review (default true)The Action ignores repo config unless a trusted config-path input is set. To
use these keys in CI, create .prowl-review.yml on the base branch, check out
that base branch to a separate path, and pass that path as config-path (for
example prowl-review-config/.prowl-review.yml). Do not point config-path at
the PR checkout; same-repo PR authors could alter review policy in their branch.
Keep draft handling out of the job-level if; a draft == false guard prevents
the Action from seeing drafts, so review.reviewDrafts: true cannot take effect.
When an auto review is skipped (paused, drafts, or auto: false) and the
merge-gate check is enabled, prowl-review posts a neutral check run so a Required
"Prowl Review" check isn't left pending.
Bot commands
Drive the reviewer from the PR by commenting @prowl-review <command> (only a
repo owner/member/collaborator is honored):
| Command | Effect |
|---|---|
| @prowl-review review | Re-review the latest changes (incremental). |
| @prowl-review full review | Re-scan the entire PR from scratch. |
| @prowl-review ignore | Reply on a finding to mute it — it won't be raised again on this PR (and repo-wide when review.repoLearnings is on) (#30). |
| @prowl-review resolve | Reply on a finding to mark its thread resolved and stop re-raising it (#26). |
| @prowl-review configure <key=value …> | Set per-PR review settings (minSeverity, maxFindings, verify); configure reset clears them (#26). |
| @prowl-review pause | Stop auto-reviewing this PR on new pushes. |
| @prowl-review resume | Re-enable auto-review. |
| @prowl-review docstrings | Draft docstrings for the changed code, posted as a copy-paste reply (#33). |
| @prowl-review tests | Draft unit-test stubs for the changed code, posted as a copy-paste reply (#33). |
| @prowl-review help | List the available commands. |
| @prowl-review <question> | Ask a free-form question — answered in-thread, grounded in the PR diff (#27). |
Anything after the mention that isn't a known command is treated as a question:
@prowl-review why is this loop O(n²)? gets a contextual reply in the same
thread. The starter below listens to top-level PR comments only; inline review
comments can be supported by also adding pull_request_review_comment, but each
inline comment creates a workflow run, so leave it out unless you need inline
questions or ignore replies.
Code assists (#33). @prowl-review docstrings drafts docstrings/doc-comments
for the functions, classes, and methods changed in the PR (in each file's
language convention); @prowl-review tests drafts unit-test stubs covering the
changed behavior, inferring the project's test framework from the diff. Both are
grounded in the (size-guarded, secret-redacted) PR diff and reply with
copy-paste-ready fenced code blocks — in-thread when invoked on an inline
comment, otherwise as a PR comment. They're suggestions to review before
committing, not auto-applied. Singular/doc/docs aliases are accepted.
Replying to findings (#22). Reply on a finding's thread and prowl-review
honors it on the next review: "won't fix" / "acknowledged" resolves the thread
and stops re-raising it. Reply "I disagree" (or "false positive", "not a
bug", …) and the judge actively re-evaluates the finding rather than silently
dropping it — it either defends it with reasoning in the thread (kept open,
still gates merge) or withdraws it, conceding and resolving the thread. Only
a repo owner/member/collaborator's reply is honored. Turn it off with
review.rejustifyDisputed: false (then a disputed finding is just withheld).
You can also reply @prowl-review resolve on a finding to mark its thread
resolved and stop re-raising it (like ignore, but it also closes the thread),
and @prowl-review configure minSeverity=major to set per-PR review settings
(minSeverity, maxFindings, verify) that apply on the next review;
@prowl-review configure reset clears them (#26). Per-PR settings are stored in
the summary's state marker and win over the repo config for that PR only.
Repo-wide learnings (#30). By default an ignore / resolve mute is scoped
to its PR. Set review.repoLearnings: true and the mute is also persisted to
a dedicated prowl-review: learned patterns tracking issue, so the same
finding is suppressed on every future PR — the OSS, BYOK equivalent of
CodeRabbit "learnings", with no external store. The issue lists each muted
pattern in plain text; delete a line (and re-run) to re-surface that finding,
or close the issue to clear the whole store. Only repo owner/member/
collaborator commands can teach it (same trust gate as every command), and the
write is best-effort — a failed issue write never blocks the per-PR mute.
Guidelines & learned patterns (#30). prowl-review injects repo guidelines
(REVIEW_GUIDELINES.md or CLAUDE.md) and a LEARNED_PATTERNS.md "do-not-raise"
file from the trusted checkout into every review. Set PROWL_ORG_GUIDELINES_PATH
to share one org-wide standard across repos — it accepts a file path or an
http(s) URL (host the file once and point every repo at it). The fetched
content is treated as untrusted prompt data just like a local file; a failed,
non-OK, or oversized fetch is skipped with a warning and the review proceeds.
Local org-guidelines file paths are confined to PROWL_ORG_GUIDELINES_WORKSPACE
when set, otherwise to the trusted Actions workspace, so a separately checked-out
org policy can be allowed without trusting arbitrary paths.
# .github/workflows/prowl-review-command.yml
name: prowl-review command
on:
issue_comment:
types: [created]
permissions:
pull-requests: write
checks: write
issues: write
contents: read
jobs:
command:
if: |
github.event.issue.pull_request &&
github.event.comment.user.type != 'Bot' &&
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
) &&
contains(github.event.comment.body, '@prowl-review')
concurrency:
group: prowl-review-${{ github.event.issue.number }}
cancel-in-progress: false
runs-on: ubuntu-latest
steps:
- id: pr
env:
GH_TOKEN: ${{ github.token }}
run: |
pr_number="${{ github.event.issue.number }}"
base_sha="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.base.sha')"
head_sha="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.head.sha')"
head_repo="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.head.repo.full_name')"
echo "base_sha=${base_sha}" >> "$GITHUB_OUTPUT"
echo "head_sha=${head_sha}" >> "$GITHUB_OUTPUT"
if [ "${head_repo}" = "${GITHUB_REPOSITORY}" ]; then
echo "trusted_head=true" >> "$GITHUB_OUTPUT"
else
echo "trusted_head=false" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
with:
ref: ${{ steps.pr.outputs.base_sha }}
- uses: actions/checkout@v4
if: steps.pr.outputs.trusted_head == 'true'
with:
ref: ${{ steps.pr.outputs.head_sha }}
path: pr-head
- uses: prowl-tools/prowl-code-review@v1
if: steps.pr.outputs.trusted_head == 'true'
env:
PROWL_REVIEWED_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
with:
mode: command
ai-key: ${{ secrets.PROWL_AI_KEY }}
config-path: .prowl-review.yml
workspace-path: ${{ github.workspace }}/pr-headFork pull requests (#20)
GitHub does not share repository secrets (your PROWL_AI_KEY) with workflows
triggered by pull_request from a fork, and the auto-provisioned
GITHUB_TOKEN is read-only there. prowl-review handles this safely:
- Default — skip with a clear message. On a fork PR with no provider key,
prowl-review skips cleanly (no failure) instead of crashing on the missing
key. The sample workflow's job-level
if: …head.repo.full_name == github.repositoryalready fences forks out; the tool-level skip is the backstop if that guard is removed. - No trust of fork code. When prowl-review does run on a fork, the fork
checkout is never trusted: repo-local linters/config don't execute
(
--trust-workspaceis force-disabled), and.prowl-review.ymlis not auto-discovered from the fork checkout — only an explicit, maintainer-setconfig-path(from the trusted base) is honored. Your key is only ever sent to your provider, never to fork code.
To review fork PRs anyway, run from a pull_request_target workflow, which
provides a write token and secrets while checking out the trusted base. Check out
the PR head to a separate path and pass it as workspace-path (context/grounding
only — still untrusted), and load config from the base:
# .github/workflows/prowl-review-forks.yml
on:
pull_request_target:
types: [opened, synchronize, ready_for_review, reopened]
permissions:
pull-requests: write
issues: write
contents: read
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # trusted base (config + guidelines)
- uses: actions/checkout@v4 # untrusted PR head, for context only
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
path: pr-head
persist-credentials: false
- uses: prowl-tools/prowl-code-review@v1
with:
ai-key: ${{ secrets.PROWL_AI_KEY }}
config-path: ${{ github.workspace }}/.prowl-review.yml
workspace-path: ${{ github.workspace }}/pr-head⚠️
pull_request_targetruns with secrets and a write token in the base repo's context. prowl-review never executes the fork's code, but you should not add other steps that run untrusted PR code in this workflow.
Branded bot identity (#59)
By default reviews post as github-actions[bot]. To post under your own
prowl-review[bot] identity with a custom avatar (like CodeRabbit/Greptile),
use a GitHub App token — no core-tool change, and no hosted service required.
- Register a GitHub App (Settings → Developer settings → GitHub Apps): give
it a name (e.g. "Prowl Review") and upload the raccoon avatar. Grant these
required permissions:
contents: read,pull requests: write,issues: write, andchecks: write. Generate a private key and install the App on your repo/org. - Add secrets:
PROWL_APP_ID(the App's id) andPROWL_APP_PRIVATE_KEY(the.pem). - Mint a token in the workflow and hand it to the Action — it already accepts
github-token+bot-login:
- name: Mint prowl-review app token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PROWL_APP_ID }}
private-key: ${{ secrets.PROWL_APP_PRIVATE_KEY }}
permission-contents: read
permission-issues: write
permission-pull-requests: write
permission-checks: write
- uses: prowl-tools/prowl-code-review@v1
with:
ai-key: ${{ secrets.PROWL_AI_KEY }}
github-token: ${{ steps.app-token.outputs.token }}
bot-login: ${{ steps.app-token.outputs.app-slug }}[bot]bot-login must match the App's login (its slug + [bot]) so update-not-duplicate
still finds prowl-review's prior comments/threads. A ready-to-copy workflow is in
examples/workflows/prowl-review-branded.yml,
and the reusable org templates pick this up automatically
when PROWL_APP_ID / PROWL_APP_PRIVATE_KEY are set (falling back to the default
token otherwise). This is the Action-path branding; the install-once hosted
App is a separate, later phase (backlog #47).
Bring your own bot identity
The branding is not baked into the tool — nothing "Prowl" or raccoon ships
inside the package. The Action just posts as whatever identity you hand it via
github-token / bot-login, so every team makes prowl-review look like their
own in-house reviewer. Pairs naturally with BYOK: your key, your bot. Pick a
tier:
| Tier | Posts as | Setup |
| --- | --- | --- |
| Default | github-actions[bot] | Nothing — works out of the box with just your AI key. |
| Your own brand | your-app[bot] + your name & avatar | Register your own GitHub App (any name/avatar you like), add your PROWL_APP_ID / PROWL_APP_PRIVATE_KEY secrets. The steps above are identical — the identity is entirely yours. |
| Local CLI | (no bot — prints to your terminal) | prowl-review locally; no GitHub identity involved. |
There's no lock-in to our raccoon: a team can register acme-review[bot] with
their own logo and nobody would know it's built on prowl-review unless they read
the workflow. The App's power lives in its private key (kept in your secrets and
never shared), so do not copy or hand out PROWL_APP_PRIVATE_KEY. A public App
can be installed across multiple owners when configured that way, but adopters
should register their own App when they need a separate identity, credential set,
or trust boundary.
Reusing one App across repos and accounts
A GitHub App is a server-side identity, not a per-device install — reuse it by installing it on more repos, not by copying anything to another machine. Where it can go depends on the App's "Where can this GitHub App be installed?" setting:
- More repos under the same owner (the account/org that owns the App): just
Install App on those repos, or set
PROWL_APP_ID/PROWL_APP_PRIVATE_KEYas org-level secrets so every repo inherits them. One App → unlimited repos. - Repos under a different owner (e.g. your personal account when the App is
org-owned): the App must be set to "Any account" (public) to install it
there — flip it via Make public at the bottom of the App's settings only
when the App owner should be trusted with the granted repository access.
Whoever controls
PROWL_APP_PRIVATE_KEYcan mint installation tokens for repos where the App is installed with the requested permissions. The alternative is to register a separate App under that owner — note App names are globally unique, so its bot login won't be identical (e.g.prowl-review-personal[bot]).
Running the CLI on another machine is unrelated: install prowl-review there
and set your AI key — the App identity is a CI concept and doesn't live on the
device.
Install (CLI)
npm install -g prowl-review # or run ad hoc with: npx prowl-review …
# Homebrew:
brew install prowl-tools/tap/prowl-reviewnpm and npx require Node.js 22.13.0 or newer within Node 22, or Node 24+; Homebrew installs node@22 automatically. The GitHub Action (above) needs no install.
Local pre-push review (CLI)
Run the same review engine against a local git diff before you open a PR — no GitHub token, no posting (#35). Findings print to the terminal:
# Review your branch's changes against main (tracked uncommitted edits included)
PROWL_AI_KEY=sk-… prowl-review review --base main
# Review the checked-out branch as an explicit head ref
PROWL_AI_KEY=sk-… prowl-review review --base main --head my-featureThe diff is taken relative to the merge base of --base and --head (PR
semantics — only the changes your branch introduces). Omit --head to review
the working tree. Untracked files are not part of Git's working-tree diff, so
local mode fails with a clear prompt to stage or commit them before review. When
--head is supplied, it must resolve to the currently checked-out HEAD and the
worktree must be clean; the later context, guidelines, grounding, and secret
scans read from that local checkout. Passing --base (or --head) switches the
review command into local mode; the GitHub flags (--pr, --repo,
--dry-run) are ignored.
| Flag | Effect |
|------|--------|
| --base <ref> | Base ref to diff against (default main). |
| --head <ref> | Checked-out head ref (default: the working tree). |
| --min-severity <sev> | Drop findings below this severity. |
| --no-context / --no-grounding / --no-verify | Skip cross-file context (#4), linter/SAST grounding (#16), or the false-positive pass (#8). |
| --trust-workspace | Allow repo-local linter/SAST code to execute. Without this or PROWL_TRUST_WORKSPACE=true, repo-local execution stays disabled. |
| --json | Print findings as JSON (for tooling) instead of the human report. |
| --no-color | Disable ANSI color (also honors NO_COLOR). |
| --fail-on <sev> | Exit non-zero when a finding at/above this severity is found — wire it into a pre-push hook as a gate. |
The same agentic cross-file context and safe linter/SAST grounding run over the
git repository top-level. Repo-local linter code executes only with
--trust-workspace or PROWL_TRUST_WORKSPACE=true; per-run cost prints to
stderr so --json stdout stays clean.
Multi-provider ensemble (#53)
A BYOK-only edge: review the same changes with more than one provider at once and consolidate their findings, so you get cross-model consensus and higher-confidence, more granular insight — something resale-based reviewers (CodeRabbit/Greptile) can't offer. Opt-in, default off.
Give each provider its own key — the cleanest setup, with no dependence on
which provider the generic key maps to. Locally (or any runner), set them as env
vars (the provider matching your primary also falls back to PROWL_AI_KEY; if
both are set, the scoped key wins):
PROWL_AI_KEY_ANTHROPIC=sk-ant-…
PROWL_AI_KEY_GEMINI=…In the GitHub Action, pass them through the per-provider inputs (each maps to
the matching PROWL_AI_KEY_<PROVIDER> env var):
- uses: prowl-tools/prowl-code-review@v1
with:
ai-key-anthropic: ${{ secrets.PROWL_AI_KEY_ANTHROPIC }}
ai-key-gemini: ${{ secrets.PROWL_AI_KEY_GEMINI }}
# ai-key-openai: ${{ secrets.PROWL_AI_KEY_OPENAI }}
config-path: prowl-review-config/.prowl-review.yml # trusted base-branch config…and list the providers in .prowl-review.yml (the first listed provider is the
primary — it runs the shared cross-file context retrieval, so put your
strongest model there):
provider: anthropic # primary (also used for the shared context pass)
ensemble:
enabled: true
providers:
- provider: anthropic
- provider: gemini
# model: gemini-2.5-pro # optional per-provider model overrideEach provider runs the full multi-pass review in parallel; the cross-file context (#4) and linters (#16) run once and are shared. A judge then consolidates findings across providers, recording provenance and boosting confidence on agreement — agreement can even rescue a finding each provider scored just under the threshold (it complements the false-positive pass, #8). Consolidated findings carry a 🤝 N/M consensus badge in the summary and an inline note naming the agreeing providers; single-provider findings are kept and attributed to the model that raised them.
See what each model said. The walkthrough leads with the consolidated, deduped table (🤝 marks agreement), then a Per-model findings area with one collapsible section per provider — Anthropic, Gemini, … — listing that model's findings in its own words and severity. Inline comments additionally keep a collapsible 🔀 N model perspectives block when models agree on a line. So you get the high-signal consensus and each model's distinct take, in one place.
Cohesive review publishing. The walkthrough is a single comment that's
updated in place on every push (not re-posted), and inline findings are
published as one GitHub COMMENT review with a comments[] batch. When the
approval gate (#52) sets an explicit Request-changes/Approve verdict, those
inline findings ride on that one verdict review.
Cost: roughly N× a single-provider review (caching helps within each provider, not across). The per-PR budget cap (#18) is split evenly across providers, and risk-tiering (#31) still applies. A provider with no key is skipped with a note; with fewer than two usable keys it runs as a normal single-provider review.
Auto-generated PR descriptions (#33)
When a pull request is opened with an empty description, prowl-review can write one from the diff — CodeRabbit-style — so reviewers get a plain-language summary of what changed. Opt-in:
# .prowl-review.yml
prDescription:
enabled: trueThe generated summary is written into the PR body between
<!-- prowl-review:pr-summary:start --> / …:end --> markers, so later pushes
refresh it in place while preserving anything you add around it. A
human-authored description is never overwritten — it only fires on an empty
body (or to refresh prowl-review's own block). Needs pull-requests: write
(already required to post reviews).
Issue / ticket validation (#32)
When a PR links a GitHub issue, prowl-review pulls the issue's acceptance criteria and flags anything the diff doesn't satisfy — so scope gaps are caught in review. Opt-in:
# .prowl-review.yml
issueValidation:
enabled: true
# maxIssues: 3 # cap linked issues fetched per PR (default 3)It recognizes these PR references for issue validation: a closing keyword
(Closes #12, Fixes owner/repo#5) or an issue URL in the PR title/body. A
dedicated requirements review lens receives the issue's criteria and raises a
finding for each one the PR misses or only partially implements — so the gaps
appear inline alongside the normal review (and, in an ensemble, get cross-model
consensus too). Fetching is tolerant: a missing or inaccessible issue (or one
that's actually a PR) is skipped with a note. Cross-tracker support
(Linear/Jira) is a future extension.
Cross-file context (#4/#5)
The biggest bug-catching lever is agentic cross-file context: instead of an
embeddings index or a vector DB, the reviewer is handed sandboxed, bounded tools
over the checked-out repo and decides what to fetch on demand — catching broken
callers, contract/interface violations, and inconsistent patterns that a
diff-only review misses. The tools are read_file, search_repo (regex grep),
list_files, and two language-aware symbol tools (#5):
find_definition— locate where a symbol (function/class/type/variable) is declared, using definition-shaped patterns per language (e.g.function/class/const,def,func,fn, Go receiver methods, assigned arrow functions, modifier-led typed methods) across the 23 detected languages — sharper than a bare grep for "where is X defined."find_references— find a symbol's call sites and other references, so the agent can see who depends on the changed code.
Both accept an optional language to narrow the search for precision and inherit
every guard of the grep primitive (repo-root confinement, symlink/ignore
rejection, binary skip, bounded matches, secret redaction, sensitive-file
skipping). This deliberately delivers AST-grade caller/definition lookup
without a tree-sitter/WASM dependency, keeping the "agentic grep, no heavy
infra" design (no indexing step, nothing extra shipped).
Semgrep SAST grounding (#16b)
Alongside ESLint (JS/TS), Ruff (Python), and Gitleaks (secrets), prowl-review runs Semgrep over changed source files and feeds its findings into the review as grounding — so the specialists reconcile with real SAST results instead of re-discovering (or hallucinating) them. It's multi-language and on by default, and skips gracefully when Semgrep isn't installed — no failure, just a note.
Ruleset sourcing. By default it runs Semgrep's curated p/default registry
pack — the rules are fetched from the registry (cached after the first run, the
same network reach osv-scanner uses for OSV.dev) with metrics disabled, so no
project metadata is ever uploaded. That's why --config auto, which phones home,
is not the default. Only Semgrep registry refs (p/…, r/…, auto) are
supported. Repository-supplied rulesets (e.g. .semgrep.yml) and remote
http(s):// configs are skipped even on trusted workspaces, since a PR could
ship or point at a malicious/noisy ruleset.
For untrusted PR scans, repository .gitignore and .semgrepignore target
filters are bypassed, and symlink targets are skipped, so explicitly changed
regular files cannot hide from SAST grounding.
# .prowl-review.yml
grounding:
semgrep:
enabled: true # default; set false to disable
config: p/default # registry pack (p/..., r/..., or auto)To use it in CI, make semgrep available on the runner (e.g. pip install
semgrep or the setup action). Without it, the rest of the review is unaffected.
Resource bounds. Linter/SAST grounding runs at most two tool runners at a time. Gitleaks file scans and Semgrep invalid-target retries are also bounded inside their runners, and every runner still honors the grounding file/finding caps so large PRs do not fan out unbounded external processes.
Dependency CVE / license scanning (#34)
When a pull request changes a dependency lockfile, prowl-review scans it with
osv-scanner v2 and surfaces known
vulnerabilities as findings (one per advisory, with the CVE id, affected
package@version, and the fixed version when available). It's part of the
deterministic grounding layer, so it runs by default and skips gracefully when
osv-scanner isn't installed — no failure, just a note. osv-scanner reads
lockfiles as data (it never executes your code), so it runs even on untrusted
checkouts. Repository-local osv-scanner.toml files are ignored for this scan so
untrusted PRs cannot suppress findings through scanner config.
Lockfiles are scanned even though they're excluded from line-by-line review by the ignore list (#19) — the scan sources changed manifests from the full diff. Supported ecosystems follow osv-scanner (npm, PyPI, Go, Cargo, Maven, Composer, RubyGems, and more).
Set an SPDX license allowlist to also flag dependencies whose license falls outside your policy:
# .prowl-review.yml
dependencyScan:
enabled: true # default; set false to disable
licenses:
allow: [MIT, Apache-2.0, BSD-3-Clause, ISC] # deps outside this list are flaggedTo use it in CI, make osv-scanner available on the runner (e.g. a setup step
before the review). Without it, the rest of the review is unaffected.
Suggested-fix validation (#39)
Findings can carry a committable suggestion block — a one-click commit
on GitHub. Because a wrong one breaks the build, prowl-review only offers that
block when it's confident in the fix:
- Confidence floor — only findings at/above
suggestions.minConfidence(default0.8) get a committable suggestion. The proposed fix for a lower-confidence finding still appears in its "Resolve with an AI agent" prompt (#57); it just isn't a one-click commit. - Structural validation — a deterministic, no-execution check drops a
suggestion that's empty, a truncation placeholder (e.g.
// ...,// rest of the code), or carries a leaked redaction marker, so a one-click apply never pastes obviously-broken code. (A valid suggestion may have unbalanced brackets — it replaces specific lines inside a block — so balance is never used to reject.)
Withheld suggestions are reported in the review notes (never dropped silently).
# .prowl-review.yml
suggestions:
minConfidence: 0.8 # raise to be stricter; lower to offer more one-click fixesResilience (#17)
Transient provider blips (a 429, a 5xx, a dropped socket) are retried with exponential backoff + jitter automatically. For sustained overload, opt into cross-generation failback:
# .prowl-review.yml
resilience:
failback:
enabled: trueWhen a review pass keeps failing with retryable/overload errors after retries
are exhausted, it retries with an older model of the same family (e.g.
claude-opus-4-8 → claude-opus-4-7, gemini-2.5-pro → gemini-2.5-flash)
before giving up — so a degraded-but-real review beats a failed pass. It never
crosses providers (that's the ensemble's job) and never falls back on a
non-retryable error; each fallback is noted in the review.
Long review runs also emit a heartbeat to the Action log (still
reviewing … (Ns elapsed)) and log transient retries across specialist,
verification, and ensemble passes, so a slow review isn't mistaken for a hung CI
job.
Debug / verbose mode (#49)
When a review behaves oddly, turn on a structured JSONL run trace to see what the run actually did — the assembled prompts, the files context retrieval pulled, the findings at each stage (raw → verified → judged), and the token/cost breakdown. Secrets are redacted (#15), and the log is appended one line per event in order without blocking review work on disk I/O.
# Local: write the trace to the default local state file (.prowl-review/debug.jsonl)
prowl-review review --base origin/main --debug
# …or to an explicit path
prowl-review review --pr 123 --debug traces/pr-123.jsonlEquivalent config / env (the flag wins):
# .prowl-review.yml
debug:
enabled: true
path: traces/run.jsonl # optional; defaults to .prowl-review/debug.jsonlPROWL_DEBUG=true PROWL_DEBUG_LOG=traces/run.jsonl prowl-review review --pr 123In the GitHub Action, set the debug: true input and upload the trace with
actions/upload-artifact to inspect it after the run. The trace path is confined
to the workspace, rejects symlinked path components, and nested parent
directories are created automatically. Secure workspace-confined writes require
POSIX O_NOFOLLOW support; platforms that do not expose it fail closed rather
than following a possible symlink. Local review ignores prowl-generated
.prowl-review/ outputs during clean-worktree checks so the default trace does
not block the next local review in repos that have not ignored that directory.
Each line is a { seq, t, event } record (t = ms since the run started). Inspect
it with jq:
# Just the assembled prompts
jq 'select(.event.type == "prompt") | .event.pass' .prowl-review/debug.jsonl
# The post-judge findings + cost
jq 'select(.event.type == "judge" or .event.type == "cost") | .event' .prowl-review/debug.jsonlDevelopment
npm install
npm run build # tsup → dist/ (CLI + library)
npm run lint # eslint
npm test # vitestThe CLI entry point is dist/cli.js (bin: prowl-review):
node dist/cli.js --helpQuality benchmark
Score the reviewer against the in-repo benchmark of PRs-with-known-bugs to
measure precision/recall/F1 and the clean-PR false-alarm rate (needs
PROWL_AI_KEY):
node dist/cli.js eval # scores ./bench, prints a summarySee docs/eval.md for details and bench/README.md for the case format.
Examples & what a review looks like
Copy-paste starters live in examples/ (auto-review workflow,
command workflow, and a .prowl-review.yml). For a rendered sample of the
published walkthrough — summary, findings table, 🤝 consensus, per-model
sections — see docs/example-review.md.
Rolling out across a whole org? Define the workflow once in your org's
.github repo and have every repo opt in with a few lines — see the reusable
workflow_call templates + per-repo callers in
examples/reusable/ (#37).
Contributing
Contributions are welcome! See CONTRIBUTING.md for setup, the
project layout, conventions, and the Definition of Done, and
CODE_OF_CONDUCT.md. The roadmap is in
docs/backlog.md.
Security & privacy
Found a vulnerability? Please report it privately — see SECURITY.md
(do not open a public issue). prowl-review is BYOK: your key is read from the
environment only, never stored or proxied, and your code only ever goes to your
chosen provider. It collects no telemetry or analytics — the only network
calls are to your LLM provider and the GitHub API.
Two pages spell out the policy in full:
docs/auth.md— the authentication & key policy: BYOK env-only keys, thePROWL_AI_*variables and precedence, how the Action passes keys as masked secrets, GitHub-token posting, and why subscription/OAuth routing isn't supported for Claude or Gemini (TOS + account-ban risk) — with OpenAI/Codex the only possible, off-by-default future exception.docs/privacy.md— data-privacy positioning: code goes only to your provider (direct, no proxy), no telemetry, secret redaction + credential-file skipping before anything is sent, and zero retention on our side.
