A run in continuous integration is governed by three requirements, and all three follow from the absence of a human. It must be non-interactive - there is nobody to ask, and any confirmation prompt turns into a stalled job that at best gets killed by a timeout. It must be limited by repository, branch and token rights - otherwise a single wrong setting repeats on every run and lives exactly as long as the pipeline file lives. And it must end with machine-readable evidence - otherwise there is nothing to check the result against except reading the log by eye, and nobody reads the log until an incident happens.
The naive approach is to take a ready example from the documentation and consider security solved by the vendor. Cursor publishes a working template, and it genuinely works: the run will start on the first attempt. But the threat model remains yours. Your environment has forks, someone else's build scripts, your own secrets and your own branching rules - everything the template cannot know about, because it was written to demonstrate a mechanism rather than to fit your organization. A ready file is a starting point, not a policy.
It helps to see a minimal review run once. Below are a trigger on a pull request with read-only rights to the contents, installing the command line, reviewing the changes with output to a file and saving the result as an artifact. The key thing here is not that the agent can read a diff but what the file does not contain: write rights, permission to change files and broad tokens. A time limit on the job stands next to the rights for the same reason - both of them cap the damage from a run that behaved differently than intended.
Rights are granted strictly for the required behavior. Review needs reading; commenting on a pull request requires write access to discussions; committing requires rights to the contents. Every widening is made a separate decision, because in continuous integration the cost of a mistake is multiplied by the number of runs: a permission granted just in case fires not once but every day and on every branch. The way back is planned from the start - rights are easier to narrow while the pipeline is new than after neighboring jobs have come to depend on them.
Pull requests from forks deserve separate caution. That is code written by an outsider, and running a privileged process on it without a separate security model is dangerous: the contents of the branch effectively govern what executes on the runner. Platforms have their own mechanics for secrets on forks, and relying on them blindly is not acceptable. The right move is to separate trusted and untrusted runs so that the untrusted one has no access to secrets at all, while everything requiring privileges runs as a separate job on already reviewed contents.
There is a subtler risk that gets forgotten: the installer itself. A line that downloads and executes a script runs on the runner with your secrets in the environment, and its contents can change between yesterday's run and today's without changing a single line in your repository. In a mature pipeline the install artifact is verified and distributed through an internal channel, and actions are pinned by version. That is the same supply chain as for the application's dependencies, only about tools - and it is usually missed, because tools do not appear in dependency reports.
Machine-readable output is needed not for tidiness but for the next step. A result file can be parsed and turned into an action: fail the job on findings above a certain severity, leave a comment, fold the findings into a repository-wide report. As long as the result stays as text in a log, none of that can be built, and the review turns into a ritual nobody reads. And it is decided in advance what exactly is done with the findings: a review that blocks a merge and a review that stays advisory are different instruments with different costs of a false positive.
Such a run has a price and a limit of applicability. Every push to a branch starts the review again, and on an active branch that means dozens of runs a day, each with its own time and its own cost; restricting by paths and events saves more here than any polishing of the prompt. The limit lies where non-determinism begins: the same change can get a review of different composition, and building a hard gate on that means getting randomly failing jobs. The sign that tells you in real work that something went wrong is usually not an error but silence: the job is green, the artifact is empty or contains the agent's refusal, and nobody notices until someone starts looking for the cause of a missed defect.
The engineering conclusion is simple: a review run and a mutating run are different pipelines with different rights. Mixing them is convenient and dangerous: add permission to change files to a review process and it turns into automatic editing without a human, on every push at that. Let review stay read-only and end with an artifact, while mutation lives separately, in an isolated process with an explicit owner, an explicit trigger and an explicit point where the changes reach human eyes.
The typical failures are predictable. Taking the template as a ready policy and leaving the rights untouched. Allowing file changes in a review job. Running a privileged pipeline on a pull request from a fork. Omitting a time limit and paying for a stalled run. And not saving a machine-readable result, leaving yourself only a log to untangle.
name: cursor-review
on:
pull_request:
permissions:
contents: read # review only needs reading
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Install the Cursor CLI
run: |
curl https://cursor.com/install -fsS | bash
echo "$HOME/.cursor/bin" >> $GITHUB_PATH
- name: Review the diff
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: agent -p --output-format json "Review this PR diff; do not edit files" > cursor-review.json
- uses: actions/upload-artifact@v4
with:
name: cursor-review
path: cursor-review.json