Skip to content

The Cursor CLI runs Cursor’s agent without the editor: agent -p "<prompt>" answers once and exits, so git hooks, GitHub Actions jobs and scheduled workflows can call it. The safe pattern keeps the agent’s step narrow, gives it no write token, and lets tests, path guards and a human merge decide whether its output ships.

This page is for developers and tech leads who already use Cursor in the editor and want the same agent working in their repository’s plumbing. You tried it once: the “AI review” job posted “Looks good overall!” on a PR that broke checkout, and the weekly dependency bot quietly edited a source file nobody asked it to touch. Both failures have the same fix, and it is not a better model.

  • A pre-commit hook that sends the staged diff to the agent and blocks on a FAIL verdict, with a warn-only switch.
  • A GitHub Actions PR review that runs on trusted code, treats the diff as data and posts from a separate job.
  • A weekly dependency job whose output is checked by a version-only guard and your test suite, in a job that holds no secrets.
  • A decision table for when a script on your runner beats Cursor Automations, the Cursor SDK or Bugbot.
  • The failure modes of unattended runs and how to recover from each.

The table lists five ways to run Cursor’s agent without you at the keyboard. This page covers the first two rows (the CLI in a git hook and in CI); the other three have their own pages.

You needUseWhy
A check at commit time, on the developer’s machineagent -p in a git hookFast feedback before code leaves the laptop; costs one call per commit
A job in your existing CI, on your runner, next to your gatesagent -p in GitHub ActionsYour secrets policy, your test suite and your required checks stay in charge
Scheduled or event-driven runs on Cursor’s machinesCloud agents and AutomationsNo runner to maintain; triggers from GitHub, Slack, Linear, Sentry and more
Agent runs started and followed from your own codeThe Cursor SDKStreaming events, cancellation and artifacts through an API
Managed PR review with no workflow fileBugbotCursor maintains the reviewer; you tune rules instead of prompts

Cursor also has its own Hooks, which run inside the agent loop and exchange JSON over stdio. They are a different feature from the git hooks below.

Install the Cursor CLI and authenticate a script

Section titled “Install the Cursor CLI and authenticate a script”
  1. Install the CLI on the machine or runner. Cursor’s installation docs (checked 2026-10-12) give curl https://cursor.com/install -fsS | bash for macOS, Linux and WSL and irm 'https://cursor.com/install?win32=true' | iex for Windows. This is the line in Cursor’s own cookbook Dockerfile, which the workflows below use:

    Terminal window
    curl -fsSL https://cursor.com/install | bash
    export PATH="$HOME/.cursor/bin:$PATH" # in GitHub Actions: echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"

    Check the install with agent --version, and update later with agent update.

  2. Create an API key in the Cursor dashboard under Integrations and store it as a secret named CURSOR_API_KEY. The CLI reads the key from that environment variable, and Cursor’s SDK examples use the same name, so a script never needs an interactive login.

  3. Run a read-only smoke test from the repository root:

    Terminal window
    agent -p "List the top-level directories of this repository and what each one holds." --output-format text
  4. Decide per job whether the agent may change files. Add --force only to jobs that must write, and run those only on a disposable runner or container with restricted network access, never on a workstation: --force approves every tool call, shell commands included; review jobs run with --mode ask, the read-only mode public review workflows use, and capture standard output instead. Omit --model to use your account’s default, and choose models from the model hub rather than hard-coding one in ten workflows.

Block risky commits with a pre-commit review

Section titled “Block risky commits with a pre-commit review”

The hook sends the staged diff inside the prompt, so the agent needs no tools and no write access. It fails open when the agent is unreachable, because a local hook is a convenience and CI is the real gate.

First, commit the reviewer prompt as a file, scripts/ai/pre-commit-review.txt, so the whole team reviews against the same text:

Then save the hook as .githooks/pre-commit, make it executable with chmod +x .githooks/pre-commit, and point git at the directory once per clone with git config core.hooksPath .githooks:

#!/usr/bin/env bash
# .githooks/pre-commit - Cursor CLI review of the staged diff
set -euo pipefail
DIFF=$(git diff --cached --diff-filter=ACMR -U3)
[ -z "$DIFF" ] && exit 0
# One command-line argument is capped at ~128 KB on Linux; CI skips such diffs too.
if [ "${#DIFF}" -gt 100000 ]; then
echo "pre-commit: diff too large for AI review, skipped. CI tests still run; split the change for AI review."
exit 0
fi
run_agent() { # macOS has no `timeout` unless coreutils is installed
if command -v timeout >/dev/null; then timeout 180 agent "$@"; else agent "$@"; fi
}
PROMPT="$(cat scripts/ai/pre-commit-review.txt)
<diff>
$DIFF
</diff>"
# --trust: this is the developer's own working tree. Without it, print mode can
# stop on the workspace-trust question on the first commit in a new clone.
if ! RESULT=$(run_agent -p "$PROMPT" --mode ask --trust --output-format text); then
echo "pre-commit: Cursor agent unavailable, commit allowed. CI still runs."
exit 0
fi
echo "$RESULT"
VERDICT=$(printf '%s\n' "$RESULT" | head -n 1 | tr -d '[:space:]*')
[ "$VERDICT" = "PASS" ] && exit 0
if [ "${AI_REVIEW_MODE:-block}" = "warn" ]; then
echo "pre-commit: findings above, commit allowed (AI_REVIEW_MODE=warn)."
exit 0
fi
echo "Commit blocked. Fix the findings, or bypass once with: git commit --no-verify"
exit 1

Three details carry the design. The tr strips Markdown bold, so **PASS** still passes. The verdict must be the literal first line, so a chatty answer blocks instead of slipping through. AI_REVIEW_MODE=warn lets a team start in advisory mode and switch to blocking once the findings prove useful.

Review pull requests in GitHub Actions without handing the agent a token

Section titled “Review pull requests in GitHub Actions without handing the agent a token”

A PR review job is where scripted agents most often leak secrets. The pull request’s branch can carry its own .cursor/ rules, MCP config and scripts, and anything the agent loads from that checkout runs next to CURSOR_API_KEY. So this workflow checks out the base commit, fetches the PR diff as plain text, runs the agent in --mode ask without --force, and posts the comment from a second job that never runs the agent.

.github/workflows/cursor-pr-review.yml
name: Cursor PR review
on:
pull_request:
types: [opened, synchronize, ready_for_review]
permissions: {}
jobs:
review:
# Fork PRs receive no secrets; skip them instead of failing.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
steps:
- name: Check out the base commit (trusted code only)
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Fetch the PR diff as data
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: gh pr diff "$PR" --repo "$GITHUB_REPOSITORY" > pr.diff
- name: Install Cursor CLI
run: |
curl -fsSL https://cursor.com/install | bash
echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"
- name: Review (ask mode, no --force, no GitHub token in this step)
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
# --trust answers the headless workspace-trust prompt for this trusted
# base checkout; confirm the flag in `agent --help` on your runner.
run: |
if [ "$(wc -c < pr.diff)" -gt 100000 ]; then
echo "Diff is too large for a single-pass review. Split the PR or review it by directory." > review.md
exit 0
fi
{ cat .github/prompts/pr-review.txt; printf '\n<diff>\n'; cat pr.diff; printf '\n</diff>\n'; } > prompt.txt
agent -p "$(cat prompt.txt)" --mode ask --trust --output-format text > review.md
test -s review.md
- uses: actions/upload-artifact@v7
with:
name: cursor-review
path: review.md
comment:
needs: review
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: write
steps:
- uses: actions/download-artifact@v8
with:
name: cursor-review
- name: Post the review (this job never runs the agent)
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body-file review.md

The prompt is read from the base checkout too, so the agent never loads a reviewer prompt or .cursor/ rules that exist only on the branch. That is not tamper-proofing, though. On the pull_request trigger GitHub runs the workflow file from the pull request’s own merge ref, so a same-repository branch can edit this YAML to check out its head, swap the prompt or echo CURSOR_API_KEY, and that edited job runs before anyone reviews it. Anyone with push access can do the same from any branch workflow, so the secret is only as safe as your list of writers. Two controls make the gate real. A CODEOWNERS entry for .github/ plus a branch rule or ruleset that requires code-owner review stops a change to the workflow or the prompt from merging unreviewed. If the job must run from a workflow the branch cannot change, use pull_request_target, which runs the default branch’s workflow; keep this exact shape there (no checkout of the head, the diff fetched as text, the fork guard) because that job holds secrets next to contributor-controlled data. Commit the prompt as .github/prompts/pr-review.txt:

The official GitHub actions/* tags above are the current majors on 2026-09-26. Pin every third-party action to a full commit SHA instead of a tag: a re-pointed tag is how the March 2025 tj-actions/changed-files compromise (CVE-2025-30066) leaked CI secrets.

Run a weekly dependency job with a version-only guard

Section titled “Run a weekly dependency job with a version-only guard”

Dependabot and Renovate already open mechanical version bumps well. The agent helps with the judgment calls: which bumps to take together, and where in your code each one deserves a look.

The workflow splits the work across three jobs, as the PR review does, because isolation between steps of one job is not real. An agent running with --force can execute shell, so it can append to $GITHUB_ENV or $GITHUB_PATH (a fake gh or git earlier on the path) and read a token that a later step in the same job receives. It can also rewrite the scripts block of package.json, which npm ci and npm test would then run.

  • propose holds CURSOR_API_KEY and a read-only token. The agent edits package.json and writes CHANGES.md, and only those two files leave the job, as an artifact. Anything else it touched is discarded with the runner.
  • test holds no secrets and starts from a fresh checkout of the default branch. A guard fails the run unless the only changes to package.json are dependency version strings, each a plain semver range. Then npm regenerates the lockfile and runs npm ci --ignore-scripts and npm test.
  • pr holds the write token. It runs neither the agent nor your code: it commits the tested package.json and lockfile and opens the PR.
.github/workflows/cursor-weekly-deps.yml
name: Weekly dependency bumps
on:
schedule:
- cron: '17 3 * * 1' # Mondays 03:17 UTC
workflow_dispatch:
permissions: {}
jobs:
propose:
# The agent job: CURSOR_API_KEY and a read-only token, nothing else.
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
changed: ${{ steps.collect.outputs.changed }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: List outdated packages and their sources (deterministic input)
# Without node_modules, npm outdated lists every package as MISSING.
run: |
npm ci --ignore-scripts
npm outdated --json > outdated.json || true
jq -r 'keys[]' outdated.json | while read -r p; do
printf '%s %s\n' "$p" "$(npm view "$p" repository.url 2>/dev/null)"
done > sources.txt
- name: Install Cursor CLI
run: |
curl -fsSL https://cursor.com/install | bash
echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"
- name: Agent edits package.json and writes CHANGES.md
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
# --trust answers the headless workspace-trust prompt for this trusted
# checkout; confirm the flag in `agent --help` on your runner.
# --force auto-approves every tool call, shell included: use it only on a
# disposable runner like this one, with no write token, and restrict the
# runner's network egress to the Cursor API and the npm registry.
run: |
agent -p "$(cat .github/prompts/deps-bump.txt)
<outdated>
$(cat outdated.json)
</outdated>
<sources>
$(cat sources.txt)
</sources>" --force --trust --output-format text
- name: Hand on exactly two files
id: collect
run: |
mkdir -p proposed
if git diff --quiet -- package.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"; exit 0
fi
test -s CHANGES.md || { echo "::error::agent did not write CHANGES.md"; exit 1; }
cp package.json CHANGES.md proposed/
echo "changed=true" >> "$GITHUB_OUTPUT"
- if: steps.collect.outputs.changed == 'true'
uses: actions/upload-artifact@v7
with:
name: deps-proposed
path: proposed/
test:
# No secrets here. The checks run on a fresh base checkout the agent never saw.
needs: propose
if: needs.propose.outputs.changed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: deps-proposed
path: proposed
- name: Guard - only dependency version strings may change
run: |
shape='del(.dependencies, .devDependencies)'
names='{d: (.dependencies // {} | keys), dd: (.devDependencies // {} | keys)}'
diff <(jq -S "$shape" package.json) <(jq -S "$shape" proposed/package.json) \
|| { echo "::error::package.json changed outside the dependency lists"; exit 1; }
diff <(jq -S "$names" package.json) <(jq -S "$names" proposed/package.json) \
|| { echo "::error::packages were added, removed or moved"; exit 1; }
# Every changed version must be a plain semver range, not a URL, path or git ref.
BAD=$(jq -r --slurpfile base package.json '
["dependencies", "devDependencies"][] as $s
| (.[$s] // {}) | to_entries[]
| select(.value != $base[0][$s][.key]) | .value' proposed/package.json \
| grep -vE '^[~^]?[0-9]+\.[0-9]+\.[0-9]+$' || true)
[ -z "$BAD" ] || { echo "::error::non-semver versions: $BAD"; exit 1; }
- name: Lockfile and tests, with install scripts off
run: |
cp proposed/package.json package.json
npm install --package-lock-only --ignore-scripts
npm ci --ignore-scripts
npm test
mkdir -p tested
cp package.json package-lock.json proposed/CHANGES.md tested/
- uses: actions/upload-artifact@v7
with:
name: deps-tested
path: tested/
pr:
# The only job with a write token. It runs neither the agent nor your code.
needs: test
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: deps-tested
- name: Open or update the PR
env:
GH_TOKEN: ${{ github.token }}
run: |
git config user.name "cursor-deps-bot"
git config user.email "cursor-deps-bot@users.noreply.github.com"
git checkout -B chore/weekly-deps
git add package.json package-lock.json
git commit -m "chore: weekly dependency bumps"
git push -f "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" chore/weekly-deps
gh pr create --head chore/weekly-deps --title "chore: weekly dependency bumps" \
--body-file CHANGES.md || gh pr edit chore/weekly-deps --body-file CHANGES.md

The guard is the verification: it compares package.json against the base with the two dependency lists removed, so a changed script, a new package or a github: URL fails the job before any install. --ignore-scripts keeps install scripts of newly bumped packages from running; if your tests need a package’s build step, run npm rebuild <package> for that named package only. For GitHub Actions to create the PR, enable Allow GitHub Actions to create and approve pull requests in the repository settings. PRs opened with GITHUB_TOKEN do not trigger other workflows, so your required checks never run on chore/weekly-deps; use a GitHub App token (actions/create-github-app-token) or a fine-grained PAT in the pr job, or close and reopen the PR, so the required checks run.

Commit the agent’s prompt as .github/prompts/deps-bump.txt. The deterministic step before it maps each package to its repository URL, so every line of CHANGES.md points the reviewer at a source instead of an invented summary:

How do you verify a scripted agent’s output without reading every line?

Section titled “How do you verify a scripted agent’s output without reading every line?”

Treat the agent’s text as one input to deterministic gates, never as the gate itself. Each workflow above maps to a check a machine enforces and a person who signs off:

WorkflowWhat proves the outputWho signs off
Pre-commit reviewLine 1 is exactly PASS; CI’s required checks run again on every pushThe committer. git commit --no-verify skips the hook and leaves no record, so CI’s required checks are what catch a skipped review
PR reviewThe review is advisory: it posts a comment and never fails the job. Required status checks (tests, types, lint) gate the merge; review.md is kept as an artifactA code owner merges; the bot never approves
Dependency jobThe version-only guard, npm ci --ignore-scripts and npm test pass in a job with no secrets; npm, not the agent, writes the lockfileThe PR reviewer, on the passing tests and the guard. CHANGES.md is an advisory summary whose lines link each package’s source

Measure the reviewer the way you would a flaky test. Once a month, label a sample of its comments as useful or noise. Git does not record --no-verify, so count bypasses indirectly: have the hook append each verdict to a log file or a metric, or count the commits where the CI review still finds blockers. When bypasses outnumber fixes, tighten the prompt or switch the hook to AI_REVIEW_MODE=warn. Keep the artifacts: they feed the evidence bundle that lets a reviewer approve a change on proof instead of line-by-line reading.

How the same jobs look in Claude Code and Codex

Section titled “How the same jobs look in Claude Code and Codex”

The workflow shape is identical in all three tools: deterministic input, a narrow agent step, deterministic gates, and a separate step that holds the write token. Only the flags differ:

Job needCursor CLIClaude Code (2.1.283)Codex (0.157.1)
Headless runagent -p "..."claude -p "..."codex exec "..."
Read-only step--mode ask (or at least omit --force); confirm in agent --help--tools "Read,Grep,Glob"-c default_permissions=":read-only" (beta)
Step that writes files--force--permission-mode acceptEdits-c default_permissions=":workspace" (beta)
Machine-readable result--output-format text plus a strict first line--output-format json, --json-schema--output-schema FILE, -o FILE
Spend ceilingjob timeout-minutes--max-budget-usdjob timeout-minutes (no budget flag in codex exec --help, 0.157.1)

The Codex column uses permission profiles, which OpenAI prefers. The legacy --sandbox read-only and --sandbox workspace-write still work in 0.157.1, but the two systems do not compose: pick one per job, never both.

Full versions of these jobs live in scripting Claude Code and running Codex non-interactively. For the permission models side by side, see permissions, sandboxes and approval modes.

What breaks when the Cursor CLI runs unattended?

Section titled “What breaks when the Cursor CLI runs unattended?”