Threat model
What a hostile pull request can try, what stops it, and what ocra does not protect against.
ocra is built to review pull requests from people you do not know. This page lists what such a pull request can try, what stops each attempt, and what remains your risk, so you can decide where to run ocra. Security and privacy describes the mechanisms in more detail.
It covers pull request reviews (the GitHub Action and ocra review --pr), GitLab merge request reviews (ocra review --mr) and local reviews with --no-repo-config. A plain local review trusts the checkout's .ocra/config.json, plugins included: pass --no-repo-config for code you do not trust.
What is protected
- Your secrets: the model provider's key, the GitHub token, and any file that is outside the reviewed revision or looks like a secret.
- Your runner or machine: ocra runs nothing from the pull request.
- Your repository: ocra posts review comments and, when configured, requests or withdraws changes. It writes nothing else.
- What ocra posts: a pull request cannot make ocra's comments hide a link's target, show images, mention people, carry hidden markup or issue commands.
- Your spend: every loop over a model has a cap.
Not promised: a correct verdict. The models read the pull request, so they can be steered, and they can be wrong.
Who can attack
| Who | What they control |
|---|---|
| The author of a pull request, from a fork or a branch | The diff and every file at its head (including .ocra/, .opencode/ and AGENTS.md), file names, commit messages, the title and the description |
| Anyone who can comment | Replies and review comments on the pull request |
| Someone with push access to your repository (on GitLab, the Developer role) | All of the above, and the pipelines their branches run: any workflow they push on GitHub, the merge request's .gitlab-ci.yml on GitLab (who controls the pipeline) |
| The model provider | Everything ocra sends it (what is sent) |
| A compromised package or registry account | Code that ocra installs |
Attacks and what stops them
| The pull request tries to | What stops it |
|---|---|
Run code: plugins in .ocra/config.json, OpenCode plugins or tools in .opencode/, install scripts, git drivers | Configuration, rules, memory and AGENTS.md come from the base commit, and repository plugins never load for pull requests. OpenCode's project configuration is off, so nothing in .opencode/ or opencode.json loads. ocra never checks out, installs or builds the pull request, and git reads it with external diff drivers and text conversion off. |
| Read secrets through a reviewer | Reviewers have no shell, file-editing or web tools: they can only read the reviewed revision. Paths stay inside the repository, symbolic links are not followed, and secret-looking files and .git/ are refused. OpenCode's process gets system basics and the model provider's credentials only (plus names you list in OCRA_RUNTIME_ENV), never GITHUB_TOKEN or other CI variables. |
| Get a secret into ocra's comments | No secret reaches a prompt, so no model can repeat one. |
| Hide an issue or argue a finding away (prompt injection) | Pull request text, code, replies and model output reach models as data, and text that could pose as ocra's own sections is rewritten. Only confirmed critical findings can block, and the judge can neither drop nor downgrade them. This limits steering; it does not prevent it (see below). |
| Forge a command or a dismissal | A command or dismissal counts only when it comes from someone with write access other than the author, and not from a comment someone else has edited. An override names the full head commit id. A thread of ocra's counts only while nobody else has edited ocra's comment in it, so the finding it is about cannot be swapped. |
| Plant markup or links in ocra's comments | Model text cannot close ocra's markup, embed images or HTML, mention people (also not through a character reference such as @), spell one of ocra's commands, start a line with a slash (which GitLab would run as a quick action, such as /merge), or become a link: addresses in it, of any scheme, are shown as text, so a pull request cannot get ocra to post an address of its choosing as a link. Code a finding quotes, in a backtick fence or an inline span, is posted as written, since a code span renders none of these; an unbalanced backtick is text, escaped so that it cannot open a span in a later part of the comment. |
| Tamper with the state in ocra's summary comment | State counts only when ocra's account posted the comment and edited it last. It is validated and its size is capped. |
| Rewrite your terminal (local runs) | Control characters, escape sequences and bidirectional overrides are replaced in everything ocra prints and in its JSON output. |
| Run up your model bill | 30 steps per task, 60 tasks per run, bounded reads and tool answers, 50 findings per task, and --max-cost-usd per run. In CI, also limit who can start a review (pull requests from forks). |
The defenses are covered by tests next to the code, in the repository.
Who controls the pipeline
ocra's rules against the author (no dismissal, no override, no forged earlier review) hold only while the author can neither change the job that runs ocra nor read its secrets. Whether they can depends on the platform and the setup.
GitHub. A pull request from a fork gets no secrets with on: pull_request, and with on: pull_request_target the workflow comes from your default branch: either way the review is out of its author's reach. Someone with push access is different. A workflow they push runs with the repository's secrets and with a GITHUB_TOKEN that posts as github-actions[bot], the account ocra trusts by default, so they could read the model key or post a summary that ocra takes for its own. GitHub pull requests shows the setup that keeps both out of their reach: the review on pull_request_target, its secrets in an environment that only the default branch may use, comments from a GitHub App whose account ocra trusts instead, and code owner review for .github/workflows.
GitLab. A merge request from a fork runs its pipeline in the fork, without your variables. A merge request from a branch of your project runs a pipeline its author wrote, and every job in it can read the project's unprotected variables: for such merge requests the review is advice that its author can forge (GitLab merge requests). A pipeline execution policy (GitLab Ultimate) protects the job, not the token.
Not tried yet, so only a design: a separate reviewer project that only maintainers can change, started by a merge request webhook through a pipeline trigger, running on its protected default branch with the token and the model key as protected variables, and reading the merge request as data. Developers could then neither change the review nor read its secrets. Its result would not be the merge request pipeline's status, so merging would still be gated by approval rules.
What ocra does not protect against
- Steering. A pull request can hide an issue from a reviewer or talk the judge out of a finding. A clean verdict proves nothing. Keep human review and security tooling for anything that must not slip through.
- The model provider. It sees the code under review. Choose one whose data policy you accept.
- Bugs in ocra or in what it builds on. ocra, OpenCode, git and Node.js all process the pull request. A flaw in one of them would run with your model key and GitHub token present. Limit what a flaw could reach: a model key used only for reviews, with a spending limit at the provider, and the token permissions in the examples.
- Your own workflow. A step that checks out and installs, builds or tests the pull request in a job that has your secrets undoes everything above.
- Push access, unless the pipeline is set up as above: people who can push write the pipelines their branches run.
- The supply chain. The Action installs the published ocra with every dependency pinned by its lockfile, install scripts off and registry signatures checked. ocra's packages are published with provenance, and OpenCode is pinned, with its built-in tools checked by a test. A stolen npm login could still publish a bad version, which two-factor authentication with a security key makes hard. Pin the Action by commit.