Skip to content

About

Local PR self-review for OpenCode - runs a multi-pass LLM review of your staged diff before the PR ever leaves your machine.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

prephlight

Local PR self-review for OpenCode. Intercepts gh pr create, runs a multi-pass LLM code review against your staged diff, and feeds blocking findings back into the same OpenCode session — before the PR ever leaves your machine.

License: MIT


What is prephlight?

Prephlight is an OpenCode plugin that brings "smart reviewer caught what the coder missed" feedback to your local workstation. When you (or your coder agent) run gh pr create, the plugin:

  1. Intercepts the command via OpenCode's tool.execute.before hook.
  2. Resolves the base branch and loads the full content of every changed file (not just the diff — full files give the reviewer real context).
  3. Detects the languages in the diff and dispatches one specialist reviewer session per language, plus one cross-cutting architecture reviewer — all running in parallel as child sessions.
  4. Consolidates findings, sorts by severity, and either:
    • Blocks the PR (CRITICAL / HIGH / MEDIUM findings) by throwing, injecting context into the coder session, appending a TUI prompt nudge, and writing a markdown report at .opencode/reviews/latest.md.
    • Passes through (LOW findings or cap reached) by appending findings to the PR body and letting gh proceed.
  5. Tracks state so the loop bounds itself: max 3 iterations on the same diff-hash, max 10 iterations on the same branch. After that, findings are appended to the PR body and the PR is allowed through.

What it is NOT:

  • Not a CI bot — review happens at PR-creation time, on your machine.
  • Not a static linter — it uses LLM reasoning, not rule sets.
  • Not telemetry — nothing leaves your workstation except calls to the LLM provider you have already configured in OpenCode.
  • Not multi-tenant — single user, single workstation.

Security & threat model

Prephlight runs locally on your machine and only calls the LLM provider you have already configured in OpenCode. Two things are worth understanding before you use it:

  • Your staged diff and the full content of every changed file are sent to your configured LLM provider. That is how the review works — the reviewer needs the code to review it. The diff is not secret-scrubbed before it is sent, so if your staged changes contain credentials, tokens, or other sensitive data, that data is included in the request to the LLM provider you configured in OpenCode. Review your diff before running gh pr create, and do not use prephlight on changes you would not paste into that provider.
  • LLM output and on-disk config/state files are treated as semi-trusted inputs. The plugin assumes the model's response and the contents of .opencode/review-config.json / .opencode/review-state.json could be hostile, and hardens against it: prototype-pollution key filtering on all JSON merges, an LLM-output sanitiser that neutralises HTML-comment hiding and tag-reconstruction in both the markdown report and the PR body, symlink refusal (O_NOFOLLOW) on file reads/writes, path-traversal allow-lists, and bounded regexes to avoid ReDoS.

Prephlight loads no third-party or attacker-supplied code: reviewers are statically imported and dispatched as data, never resolved by id or path.


Quick install

Prephlight is installed as a local OpenCode plugin by symlinking the repo into OpenCode's plugin directory.

git clone https://github.com/mguttmann/prephlight.git
cd prephlight
bun install
ln -s "$(pwd)" ~/.config/opencode/plugins/prephlight
# Restart OpenCode (or reload the session) for the plugin to register.

Verify the install:

bunx tsc --noEmit   # type-check passes
bun test            # tests pass

Then add the plugin to your opencode.json — see examples/opencode-config-snippet.json.


How it works (high-level flow)

gh pr create
     │
     ▼
[ tool.execute.before ]   ← prephlight hook
     │
     ▼
bypass / config check   ── skip? ──▶ pass-through
     │
     ▼
diff + state load
     │
     ▼
counter at cap? ── yes ──▶ append findings to PR body, pass-through
     │ no
     ▼
language detection ──▶ knowledge file loading (3-tier lookup)
     │
     ▼
parallel reviewers (one session per language + 1 architecture)
     │
     ▼
findings consolidation
     │
     ├── blocking?  ──▶ markdown + session.prompt + appendPrompt + toast + throw
     │
     └── non-blocking? ──▶ markdown + append findings to PR body + let gh run

The reviewer prompts are built from three deterministic inputs:

  1. Severity definitions (CRITICAL / HIGH / MEDIUM / LOW), passed verbatim to every reviewer so severity is applied consistently and never paraphrased.
  2. Knowledge files — language-specific review heuristics shipped in knowledge/, with 3-tier override (repo > user > built-in).
  3. The changed files (full content + unified diff) plus PR title and draft body.

Each reviewer returns structured JSON validated by Zod (FindingsResponseSchema in src/reviewers/findings-parser.ts). Invalid output is treated as "no findings" with a warning log — we never block the user on a parser failure.


Configuration

Prephlight reads four configuration sources, in order of precedence (highest first):

  1. Environment variables (per-command overrides — see below).
  2. <repo>/.opencode/review-config.json (repo-level).
  3. ~/.config/opencode/review-config.json (user-global).
  4. Built-in defaults.

A sample repo-level config lives at examples/review-config-snippet.json.

Available config keys

Key Default Effect
enabled true Master kill-switch for the plugin in this repo.
blockingSeverities ["CRITICAL","HIGH","MEDIUM"] Which severities abort gh pr create.
maxIterationsPerDiff 3 Cap before findings auto-pass-through on the same diff-hash.
maxIterationsPerBranch 10 Branch-level cap (independent of diff-hash cap; resets on a clean review).
maxFilesInDiff 25 Soft top-N cap: above this, the lowest-LOC files are reviewed, rest dropped.
maxLocInDiff 50000 Aggregate LOC ceiling across reviewed files (top-N policy, same as above).
reviewerTimeoutSeconds 90 Per-reviewer wall-clock timeout.
skipPatterns docs/lockfiles/configs (see schema) Globs that don't count as reviewable changes.
includeArchitectureReviewer true Whether to also spawn the cross-cutting reviewer.
notifications.macosToast true Toast notification on blocking findings.
notifications.macosSound true Sound chime on CRITICAL only.
notifications.macosSoundName "Funk" Built-in macOS system sound used when macosSound fires.
model.providerID "anthropic" Reviewer model provider (defaults to Anthropic).
model.modelID "claude-opus-4-8" Reviewer model id (temperature is hard-coded to 0 regardless).
agentName "prephlight-reviewer" Agent definition used for reviewer sessions.
languages.aliases {} Per-repo extension overrides (e.g. {".mjs":"javascript"}) for the detector.

Bypass

Three independent ways to skip review, in increasing scope:

1. Per-command via environment

PREPHLIGHT_SKIP=1 PREPHLIGHT_SKIP_REASON="docs-only follow-up" \
  gh pr create --title "Fix typo in README" --body "..."

PREPHLIGHT_SKIP_REASON is mandatory when PREPHLIGHT_SKIP=1. The reason is logged into a bounded ring buffer in .opencode/review-state.json so you have an audit trail of every skip you've taken.

2. Per-repo via config

{ "enabled": false }

in .opencode/review-config.json. Useful for repos where the plugin doesn't fit (e.g., docs-only repos, archived projects).

3. Per-file-pattern via config

{ "skipPatterns": ["docs/**", "*.md", "CHANGELOG*"] }

If a PR only touches files matching one of these patterns, review is skipped silently and the PR proceeds.

Environment variables (full list)

Variable Effect
PREPHLIGHT_SKIP Set to 1 to bypass review for the next gh pr create. Requires reason.
PREPHLIGHT_SKIP_REASON Required when PREPHLIGHT_SKIP=1. Logged into state.
PREPHLIGHT_DISABLED Set to 1 to disable the plugin for the entire OpenCode session.
PREPHLIGHT_DEBUG Set to 1 for verbose logging via client.app.log.

Known limitations

Security

  • Prototype-pollution guard. Configuration and state files are JSON-parsed with a __proto__ / constructor / prototype key filter to prevent prototype-pollution attacks via a malicious .opencode/review-config.json or .opencode/review-state.json. (Side effect: a future config or state schema field named constructor/prototype/__proto__ would be silently dropped during merge.)
  • LLM output is treated as untrusted. The sanitiser is a single-pass state machine that strips <!-- and --> delimiters and HTML-encodes < / > in both the local markdown report and the GitHub PR body — protecting against tag-reconstruction bypasses (<sc<!---->ript> cannot become <script>), HTML-comment hiding in markdown previews, and regex DoS on prompt-injected payloads. The sanitiser runs over title, file, rationale, scenario, suggestion, summary, references[], and contextNotes[].
  • --body-file symlink refusal. Prephlight refuses to read or write through --body-file paths that resolve to symbolic links. Use a regular file or pipe the body via --body "..." instead.
  • Free-form field bounds. rationale, scenario, suggestion are capped at 8 192 bytes; file at 1 024; references[] and contextNotes[] items at 2 048; reviewer at 128. Inputs over the cap are dropped with a contextNotes warning.

Tool / API

  • First-message hook miss (OpenCode bug #6862). The tool.execute.before hook may not fire on the very first tool call of a fresh session. Re-running the command intercepts normally.
  • Quoted invocations. A bash -c "gh pr create ..." wrapper hides the command inside a quoted string. The matcher only catches unquoted invocations.
  • Editor-mode gh pr create. When gh pr create is invoked with no body-providing flag, gh opens $EDITOR for the user. Prephlight bypasses review in this case (it cannot inject findings into an editor that hasn't opened yet).

Filesystem & operational hygiene

  • latest.md is a regular file, not a symlink. The refresh sequence uses O_NOFOLLOW + O_EXCL on a temp file, lstats the destination, refuses to rename through a symlink, and atomically renames the temp onto latest.md.
  • Stale-tmp-file sweep. On every writeFindingsMarkdown entry, the plugin scans .opencode/reviews/ for latest.md.tmp-<pid>-<nonce> files older than ~1 hour and best-effort deletes them. Younger temp files are left alone in case a concurrent write is in flight.
  • fd.close() errors no longer mask write errors. Every fd.close() in the output layer is wrapped in .catch(() => {}) so the original write/read error survives and the underlying fd cannot leak.
  • languages.aliases is case-insensitive. {".kt": "Kotlin"} works as expected; the language id is lowercased before the ASCII allow-list regex.

Resource budgeting

  • Counter-on-gh-failure. The iteration and branch counters are incremented before gh pr create actually runs. If gh itself fails (token expired, network down, repo state issue), the counter remains incremented. Re-running gh pr create after fixing the unrelated issue may show fewer remaining iterations than expected.
  • Reviewer timeout default = 90s. A single reviewer hitting the timeout produces a blocking MEDIUM finding. For very large PRs, raise reviewerTimeoutSeconds in .opencode/review-config.json or split the PR.

State

  • Single writer per repo. .opencode/review-state.json is rewritten atomically per review run, but concurrent runs against the same repo can produce a race. The race-merge is best-effort; a narrow TOCTOU window inside the write itself is documented and accepted as a v0.1 limitation.
  • State reset on schema change. Future prephlight versions may add required state fields. The plugin falls back to fresh state on validation failure and logs a warning — but iteration counters reset to 0.

Body channel quirks

  • Stdin body silently discarded. gh pr create --body-file - (stdin) gets rewritten to --body "<findings>". The original stdin content is NOT preserved (we cannot read stdin twice from inside tool.execute.before).
  • --fill-first uses git history. Both --fill and --fill-first use git log to derive title and body. Prephlight aggregates all commits in the range; gh's exact --fill-first semantics may differ in edge cases (multi-line first-commit subjects).

Platform

  • macOS-only sound notifications. Toast notifications fall back to the TUI on Linux. The osascript-based sound is macOS-only and silently skipped on other platforms.

Branch tracking

  • Detached HEAD. PRs from detached HEAD use the commit SHA as the branch identifier. The default 10-iteration branch cap applies per unique SHA.

Reviewer scope

  • Single-reviewer-per-language model. Each language gets one specialist reviewer per PR. Multi-iteration runs spawn fresh reviewers; child sessions are deleted on completion (success or timeout) but if client.session.delete is unavailable in the SDK, the sessions may linger until OpenCode restart.

Architecture overview

prephlight/
├── src/
│   ├── plugin.ts                # entry; hook registration
│   ├── logging.ts               # client.app.log wrapper
│   ├── trigger/                 # gh pr create detection + bypass
│   ├── analysis/                # base-branch, diff, languages, knowledge
│   ├── reviewers/               # orchestrator + per-role drivers
│   ├── state/                   # review-state.json read/write/GC
│   ├── config/                  # config merge + Zod schema
│   ├── output/                  # markdown, session-feedback, PR-body
│   └── prompts/                 # severity + base prompts
├── knowledge/                   # default reviewer heuristics per language
├── agent/                       # shared reviewer agent definition
├── examples/                    # opencode.json + review-config snippets
└── tests/                       # unit + integration + manual e2e

Each module has a single responsibility and a clear contract, so the codebase can be navigated by responsibility rather than by file count:

  • trigger/ decides whether a given gh pr create invocation should be intercepted at all (command matching + the three bypass paths).
  • analysis/ resolves the base branch, loads the diff and full changed files, detects languages, and loads the matching knowledge files.
  • reviewers/ orchestrates the parallel reviewer sessions, validates each reviewer's JSON, and consolidates findings by severity.
  • state/ reads, writes, and garbage-collects the iteration/branch counters and the bypass audit ring buffer.
  • config/ performs the four-tier configuration merge against the Zod schema.
  • output/ produces the markdown report, injects context into the coder session, and rewrites the PR body for pass-through.

Contributing

This is an early v0.1.x plugin and APIs may shift. See CONTRIBUTING.md for the full development setup and conventions. In short:

  1. Open an issue describing the change before opening a PR for anything non-trivial. The plugin is small and opinionated; we'd rather discuss shape than redo work.
  2. All TypeScript code is strict mode, no any — use unknown + narrowing.
  3. All module boundaries that accept external input use Zod for validation.
  4. Tests run with bun test; type-checking is bunx tsc --noEmit.
  5. Production code paths never write to stdout or stderr — use src/logging.ts (createLogger(client)).

License

MIT © 2026 Manuel Guttmann

About

Local PR self-review for OpenCode - runs a multi-pass LLM review of your staged diff before the PR ever leaves your machine.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages