tools Β· Β· 15 min read

Automated PR Reviews with Cursor AI and GitHub Actions

Set up an AI-powered code review bot using Cursor CLI in GitHub Actions. Get thorough, context-aware feedback on every pull request automatically.

Manual code reviews are time-consuming and inconsistent. What if you could have an AI reviewer that checks every PR for security issues, performance problems, and code quality β€” automatically? This guide shows you how to set up Cursor AI as your automated PR reviewer using GitHub Actions.


What You’ll Build

A GitHub Actions workflow that:

  • Automatically reviews PRs when opened
  • Can be triggered manually with /review comment
  • Checks for security vulnerabilities, performance issues, and code quality
  • Posts inline comments on specific lines
  • Optionally blocks PRs with critical issues

Prerequisites

  • A GitHub repository
  • Cursor API key (get it from cursor.com)
  • Basic understanding of GitHub Actions

πŸ“š Official Docs: Cursor CLI GitHub Actions Integration


How It Works

PR Opened / /review comment
        ↓
  GitHub Actions triggered
        ↓
  Checkout code + Install Cursor CLI
        ↓
  Cursor Agent analyzes diff
        ↓
  Posts inline comments via GitHub API
        ↓
  (Optional) Blocks PR if critical issues found

The Workflow File

Create .github/workflows/cursor_pr_review.yml:

name: cursor-pr-review

on:
  pull_request:
    types: [opened, ready_for_review]
  issue_comment:
    types: [created]

permissions:
  pull-requests: write
  contents: read
  issues: write

Triggers Explained

EventWhen it fires
pull_request: openedNew PR is created
pull_request: ready_for_reviewDraft PR marked as ready
issue_comment: createdSomeone comments on PR

The issue_comment trigger allows manual reviews via /review command.


Step-by-Step Breakdown

1. Job Conditions

jobs:
  code-review:
    if: >
      (
        github.event_name == 'pull_request' &&
        github.event.action == 'opened'
      ) ||
      (
        github.event_name == 'issue_comment' &&
        github.event.issue.pull_request &&
        contains(github.event.comment.body, '/review') &&
        (
          github.event.comment.author_association == 'OWNER' ||
          github.event.comment.author_association == 'MEMBER' ||
          github.event.comment.author_association == 'COLLABORATOR'
        )
      )
    timeout-minutes: 15
    runs-on: ubuntu-latest

This job runs when:

  • A PR is opened, OR
  • Someone with repo access comments /review

The author association check prevents random users from triggering expensive AI reviews.


2. Get PR Details

- name: Get PR details
  id: pr-vars
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    # Get PR number based on event type
    if [ "${{ github.event_name }}" == "issue_comment" ]; then
      PR_URL="${{ github.event.issue.pull_request.url }}"
      PR_NUMBER=$(echo "$PR_URL" | sed 's/.*\/pulls\///')
    else
      PR_NUMBER=${{ github.event.pull_request.number }}
    fi
    
    echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
    
    # Extract additional instructions from comment
    ADDITIONAL_INSTRUCTIONS=""
    if [ "${{ github.event_name }}" == "issue_comment" ]; then
      COMMENT_BODY="${{ github.event.comment.body }}"
      ADDITIONAL_INSTRUCTIONS=$(echo "$COMMENT_BODY" | sed "s|.*/review||" | xargs)
    fi

What’s happening:

  • Extracts PR number from different event types
  • Parses additional instructions from /review focus on security style comments
  • Gets head and base SHA for diff comparison

3. Checkout & Install Cursor

- name: Checkout repository
  uses: actions/checkout@v4
  with:
    fetch-depth: 0
    ref: ${{ steps.pr-vars.outputs.pr_head_sha }}

- name: Install Cursor CLI
  run: |
    curl https://cursor.com/install -fsS | bash
    echo "$HOME/.cursor/bin" >> $GITHUB_PATH

Key points:

  • fetch-depth: 0 β€” Full git history for accurate diffs
  • ref: pr_head_sha β€” Checkout the PR branch, not main
  • Cursor CLI is installed fresh each run

4. Get Changed Files

- name: Get additional PR details
  id: pr-details
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    PR_NUMBER="${{ steps.pr-vars.outputs.pr_number }}"
    
    # Get PR metadata
    PR_DATA=$(gh pr view "$PR_NUMBER" --json title,body,additions,deletions,changedFiles)
    
    # Get changed files
    CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --name-only)

Uses GitHub CLI (gh) to fetch:

  • PR title and description
  • Number of additions/deletions
  • List of changed files

5. The AI Review (The Magic Part) πŸͺ„

- name: Perform automated code review
  env:
    CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
    MODEL: opus-4.5-thinking
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    BLOCKING_REVIEW: ${{ vars.BLOCKING_REVIEW || 'false' }}
  run: |
    agent --force --model "$MODEL" --output-format=text --print <<PROMPT_EOF
    You are a senior software engineer performing a comprehensive code review...
    PROMPT_EOF

The agent command runs Cursor’s AI with a detailed prompt that instructs it to:

Analyze multiple dimensions:

CategoryWhat it checks
🚨 CriticalNull dereferences, resource leaks, data corruption
πŸ”’ SecuritySQL injection, XSS, hardcoded secrets, auth bypasses
⚑ PerformanceN+1 queries, missing indexes, blocking I/O
πŸ—οΈ ArchitectureCoupling, SRP violations, API design
πŸ“– Code QualityNaming, dead code, DRY violations
πŸ§ͺ TestingMissing tests, edge cases, assertions

Comment format:

πŸ”’ **SQL Injection vulnerability**

The `user_id` is being concatenated directly into the query string.
This opens up the endpoint to SQL injection attacks.

Use parameterized queries instead:

cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))

6. Blocking Review (Optional)

- name: Check blocking review results
  if: env.BLOCKING_REVIEW == 'true'
  run: |
    if [ "${CRITICAL_ISSUES_FOUND:-false}" = "true" ]; then
      echo "❌ Critical issues found. Failing the workflow."
      exit 1
    fi

When BLOCKING_REVIEW is enabled:

  • Critical (🚨) or security (πŸ”’) issues will fail the workflow
  • Can be used with branch protection to block merging
  • Set via repository variable: vars.BLOCKING_REVIEW

Security: Restricting Agent Permissions

Create .cursor/cli.json in your repo root:

{
  "permissions": {
    "allow": [],
    "deny": ["Shell(git push)", "Shell(gh pr create)", "Write(**)"]
  }
}

Why this matters:

Denied PermissionReason
Shell(git push)Prevent agent from pushing code
Shell(gh pr create)Prevent creating new PRs
Write(**)Prevent modifying any files

The agent should only read and comment β€” never modify code.


Required Secrets & Variables

Secrets (Settings β†’ Secrets β†’ Actions)

NameDescription
CURSOR_API_KEYYour Cursor API key

GITHUB_TOKEN is automatically provided by GitHub Actions.

Variables (Settings β†’ Variables β†’ Actions)

NameDefaultDescription
BLOCKING_REVIEWfalseSet to true to fail on critical issues

Usage

Automatic Review

Just open a PR. The workflow triggers automatically.

Manual Review

Comment on any PR:

/review

With specific focus:

/review focus on security and error handling
/review check for memory leaks

The additional instructions are passed to the AI for targeted review.


Example Output

When the workflow runs, you’ll see:

Inline comments on specific lines:

πŸ”’ Potential XSS vulnerability

The username is inserted directly into innerHTML without sanitization. Use textContent instead, or sanitize with DOMPurify.

Summary comment:

πŸ“‹ PR Overview

This PR adds user search functionality to the dashboard. New SearchBar component with debounced API calls, results displayed in UserList.

πŸ’‘ Suggestions

  • Add loading state for better UX
  • Consider caching search results

Cost Considerations

  • Each review consumes API tokens based on diff size
  • opus-4.5-thinking is powerful but expensive
  • For smaller projects, consider claude-sonnet or gpt-4o
  • Use /review manual trigger to control costs

Tips for Better Reviews

  1. Write good PR descriptions β€” AI uses them for context
  2. Keep PRs small β€” Better analysis, lower cost
  3. Use conventional commits β€” Helps AI understand intent
  4. Configure .cursor/rules β€” Add project-specific guidelines
  5. Read the docs β€” Check Cursor CLI documentation for more examples

Troubleshooting

Review not triggering?

  • Check if user has correct association (OWNER/MEMBER/COLLABORATOR)
  • Verify CURSOR_API_KEY is set correctly
  • Check workflow permissions in repo settings

Comments not appearing?

  • Ensure pull-requests: write permission is set
  • Check GitHub Actions logs for API errors

Taking too long?

  • Increase timeout-minutes (default: 15)
  • Use a faster model
  • Review smaller PRs

Full Workflow File

Here’s the complete workflow for copy-paste.

name: cursor-pr-review

on:
  pull_request:
    types: [opened, ready_for_review]
  issue_comment:
    types: [created]

permissions:
  pull-requests: write
  contents: read
  issues: write

jobs:
  code-review:
    if: >
      (
        github.event_name == 'pull_request' &&
        github.event.action == 'opened'
      ) ||
      (
        github.event_name == 'issue_comment' &&
        github.event.issue.pull_request &&
        contains(github.event.comment.body, '/review') &&
        (
          github.event.comment.author_association == 'OWNER' ||
          github.event.comment.author_association == 'MEMBER' ||
          github.event.comment.author_association == 'COLLABORATOR'
        )
      )
    timeout-minutes: 15
    runs-on: ubuntu-latest
    steps:
      - name: Get PR details
        id: pr-vars
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Get PR number based on event type
          if [ "${{ github.event_name }}" == "issue_comment" ]; then
            PR_URL="${{ github.event.issue.pull_request.url }}"
            PR_NUMBER=$(echo "$PR_URL" | sed 's/.*\/pulls\///')
          else
            PR_NUMBER=${{ github.event.pull_request.number }}
          fi
          
          echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
          
          # Extract additional instructions from comment (if triggered by comment)
          ADDITIONAL_INSTRUCTIONS=""
          if [ "${{ github.event_name }}" == "issue_comment" ]; then
            COMMENT_BODY="${{ github.event.comment.body }}"
            ADDITIONAL_INSTRUCTIONS=$(echo "$COMMENT_BODY" | sed "s|.*/review||" | xargs)
            if [ -n "$ADDITIONAL_INSTRUCTIONS" ]; then
              echo "additional_instructions<<EOF" >> "$GITHUB_OUTPUT"
              echo "$ADDITIONAL_INSTRUCTIONS" >> "$GITHUB_OUTPUT"
              echo "EOF" >> "$GITHUB_OUTPUT"
            fi
          fi
          
          # Get PR details using GitHub API (works before checkout)
          if [ "${{ github.event_name }}" == "pull_request" ]; then
            PR_HEAD_SHA="${{ github.event.pull_request.head.sha }}"
            PR_BASE_SHA="${{ github.event.pull_request.base.sha }}"
          else
            PR_DATA=$(curl -s -H "Authorization: token $GH_TOKEN" \
              -H "Accept: application/vnd.github.v3+json" \
              "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER")
            PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha')
            PR_BASE_SHA=$(echo "$PR_DATA" | jq -r '.base.sha')
          fi
          
          echo "pr_head_sha=$PR_HEAD_SHA" >> "$GITHUB_OUTPUT"
          echo "pr_base_sha=$PR_BASE_SHA" >> "$GITHUB_OUTPUT"

      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: ${{ steps.pr-vars.outputs.pr_head_sha }}

      - name: Install Cursor CLI
        run: |
          curl https://cursor.com/install -fsS | bash
          echo "$HOME/.cursor/bin" >> $GITHUB_PATH

      - name: Configure git identity
        run: |
          git config user.name "Cursor Agent"
          git config user.email "cursoragent@cursor.com"

      - name: Get additional PR details
        id: pr-details
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          PR_NUMBER="${{ steps.pr-vars.outputs.pr_number }}"
          
          # Get PR details using gh CLI (now that we have git repo)
          PR_DATA=$(gh pr view "$PR_NUMBER" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)
          echo "pr_data<<EOF" >> "$GITHUB_OUTPUT"
          echo "$PR_DATA" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
          
          # Get changed files
          CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --name-only)
          echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
          echo "$CHANGED_FILES" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"

      - name: Perform automated code review
        env:
          CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
          MODEL: opus-4.5-thinking
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BLOCKING_REVIEW: ${{ vars.BLOCKING_REVIEW || 'false' }}
          ADDITIONAL_INSTRUCTIONS: ${{ steps.pr-vars.outputs.additional_instructions }}
        run: |
          ADDITIONAL_CONTEXT=""
          if [ -n "$ADDITIONAL_INSTRUCTIONS" ]; then
            ADDITIONAL_CONTEXT="- Additional Instructions: $ADDITIONAL_INSTRUCTIONS"
          fi
          
          agent --force --model "$MODEL" --output-format=text --print <<PROMPT_EOF
          You are a senior software engineer performing a comprehensive code review in a GitHub Actions runner. The gh CLI is authenticated via GH_TOKEN. Your goal is to provide high-quality, constructive feedback that helps improve the codebase and mentor the author.

          Context:
          - Repo: ${{ github.repository }}
          - PR Number: ${{ steps.pr-vars.outputs.pr_number }}
          - PR Head SHA: ${{ steps.pr-vars.outputs.pr_head_sha }}
          - PR Base SHA: ${{ steps.pr-vars.outputs.pr_base_sha }}
          - Blocking Review: ${{ env.BLOCKING_REVIEW }}
          ${ADDITIONAL_CONTEXT}

          Objectives:
          1) Re-check existing review comments and reply resolved when addressed.
          2) Review the current PR diff comprehensively across multiple dimensions.
          3) Provide constructive, actionable feedback with context and suggestions.
          4) Help the author learn and improve through explanatory comments.

          Information Gathering:
          - Get PR metadata: gh pr view --json title,body,labels,milestone,closingIssuesReferences,commits,comments,author
          - Get commit messages: gh pr view --json commits --jq '.commits[].messageHeadline'
          - Get diff: gh pr diff
          - Get changed files with patches: gh api repos/${{ github.repository }}/pulls/${{ steps.pr-vars.outputs.pr_number }}/files --paginate --jq '.[] | {filename,patch,status,additions,deletions}'
          - Understand the purpose from PR title, body, linked issues, and commit messages
          - Read relevant source files to understand the broader context around the changes
          ${ADDITIONAL_INSTRUCTIONS:+$'\n          - ADDITIONAL INSTRUCTIONS FROM REVIEWER (if provided): '${ADDITIONAL_INSTRUCTIONS}}

          Analysis Dimensions (BE STRICT on security, performance, architecture, code quality):

          🚨 Critical Issues (MUST fix):
            - Null/undefined dereferences
            - Resource leaks (unclosed files, connections, streams)
            - SQL/NoSQL injection vulnerabilities
            - XSS and other injection attacks
            - Authentication/authorization bypasses
            - Hardcoded secrets or credentials
            - Race conditions and deadlocks
            - Data corruption risks
            - Unhandled exceptions in critical paths

          πŸ”’ Security & Privacy:
            - Input validation and sanitization
            - Proper encoding/escaping of outputs
            - Secure handling of sensitive data (PII, credentials)
            - CORS and CSRF protection
            - Secure defaults and fail-safe designs
            - Proper use of cryptographic functions
            - Audit logging for sensitive operations

          ⚑ Performance:
            - N+1 query patterns
            - Missing indexes for frequent queries
            - Unnecessary allocations in hot paths
            - Blocking I/O in async contexts
            - Missing pagination for large datasets
            - Inefficient algorithms (O(nΒ²) where O(n) is possible)
            - Memory leaks and unbounded growth
            - Missing caching opportunities

          πŸ—οΈ Architecture & Design:
            - Single Responsibility Principle violations
            - Tight coupling between modules
            - Proper abstraction levels
            - Consistent patterns with existing codebase
            - Appropriate use of design patterns
            - API design and backward compatibility
            - Database schema design and migrations

          πŸ“– Code Quality & Maintainability:
            - Code readability and clarity
            - Meaningful variable/function naming
            - Dead code or unreachable paths
            - Magic numbers without constants
            - Overly complex logic that could be simplified
            - DRY violations (duplicated code)
            - Proper separation of concerns

          πŸ§ͺ Testing:
            - Missing unit tests for new logic
            - Missing edge case coverage
            - Test assertions that actually verify behavior
            - Mocking best practices
            - Integration test considerations
            - Suggest specific test scenarios if tests are missing

          πŸ“š Documentation:
            - Missing or outdated comments for complex logic
            - API documentation (JSDoc, docstrings, etc.)
            - README updates if applicable
            - Inline comments explaining "why" not "what"

          β™Ώ Accessibility & Internationalization (for UI code):
            - ARIA labels and semantic HTML
            - Keyboard navigation support
            - Color contrast and visual accessibility
            - Hardcoded strings that should be localized

          Review Procedure:
          - Compute exact inline anchors for each issue (file path + diff position)
          - Comments MUST be placed inline on the changed line in the diff
          - Detect prior "no issues" comments (match: "βœ… no issues", "No issues found", "LGTM")
          - If CURRENT run finds issues and prior "no issues" comments exist:
            - Delete via: gh api -X DELETE repos/${{ github.repository }}/issues/comments/<comment_id>
            - Or for PR comments: gh api -X DELETE repos/${{ github.repository }}/pulls/${{ steps.pr-vars.outputs.pr_number }}/comments/<comment_id>
            - If deletion fails, edit to prefix "[Superseded by new findings]"
          - If a previously reported issue appears fixed, reply: βœ… This issue appears to be resolved
          - Avoid duplicates: skip if similar feedback exists on nearby lines

          Commenting Guidelines:
          - Max 15 inline comments total; prioritize by impact (security > performance > architecture > quality)
          - One issue per comment; place on the exact changed line
          - Write naturally like a human reviewer - explain the issue, why it matters, and how to fix
          - Provide enough context so the author understands the root cause
          - Include code example for the fix when applicable
          - For subjective suggestions, phrase as "Consider..." or "You might want to..."
          - Use emojis to categorize:
            🚨 Critical (blocks merge)
            πŸ”’ Security concern
            ⚑ Performance improvement
            ⚠️ Potential bug or logic issue
            πŸ—οΈ Architecture/design suggestion
            πŸ“– Readability/maintainability
            πŸ§ͺ Testing suggestion
            πŸ’‘ Tip or best practice
            βœ… Resolved

          Comment Format Example:
          "πŸ”’ **SQL Injection vulnerability**
          
          The \`user_id\` is being concatenated directly into the query string here. This opens up the endpoint to SQL injection - an attacker could pass something like \`1; DROP TABLE users--\` and it would execute.
          
          Use parameterized queries instead:
          \`\`\`python
          cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))
          \`\`\`"

          Summary Comment Guidelines:
          - Start with "# πŸ“‹ PR Overview" header
          - Write 2-4 sentences explaining what this PR does based on CODE DIFF (not just PR description)
          - Focus on: what changed, why, and key files/functions modified
          - Only add "## πŸ’₯ Breaking Changes" section if there are actual breaking changes:
            - API contract changes (request/response schema, endpoints)
            - Database schema changes (migrations needed)
            - Environment variables added/removed/renamed
            - Configuration format changes
          - Only add "## πŸ”„ Impact" section if there's significant impact:
            - Downstream services that need updates
            - Data migrations required
            - Rollback considerations
          - Only add "## πŸ’‘ Suggestions" section for HIGH-PRIORITY suggestions NOT already in inline comments:
            - Missing unit tests for critical logic
            - Potential bugs or edge cases to handle
            - Security hardening recommendations
            - Performance improvements for hot paths
            - Error handling gaps
          - Do NOT duplicate inline comments in suggestions - if already commented inline, skip it
          - Example:
            "# πŸ“‹ PR Overview
            This PR adds JWT authentication to replace session-based auth. New \`auth/jwt.py\` handles token generation and validation, \`routes/login.py\` now returns tokens instead of cookies.
            
            ## πŸ’₯ Breaking Changes:
            - \`POST /login\` response changed to \`{access_token, refresh_token}\`
            - New required env: \`JWT_SECRET\`
            
            ## πŸ’‘ Suggestions
            - Add unit tests for token expiration and refresh logic
            - Consider rate limiting on token refresh endpoint"

          Submission:
          - If NO issues and NO prior comment: submit brief positive summary
          - If issues exist and prior "no issues" comment exists: clean it up first
          - Submit ONE review with inline comments + summary via GitHub Reviews API:
            - For single-line comment: { "path": "<file>", "line": <line_number>, "side": "RIGHT", "body": "..." }
            - For multi-line comment (when issue spans multiple lines): { "path": "<file>", "start_line": <start>, "line": <end>, "side": "RIGHT", "body": "..." }
            - Use multi-line comments when: function/block spans lines, related changes are grouped, or context helps understanding
            - "side": "RIGHT" for new code (additions), "LEFT" for old code (deletions)
            - Submit: gh api repos/${{ github.repository }}/pulls/${{ steps.pr-vars.outputs.pr_number }}/reviews -f event=COMMENT -f body="\$SUMMARY" -f comments='[\$COMMENTS_JSON]'
          - Do NOT use: gh pr review --approve or --request-changes

          Blocking Behavior:
          - If BLOCKING_REVIEW is true and any 🚨 or πŸ”’ issues posted: echo "CRITICAL_ISSUES_FOUND=true" >> \$GITHUB_ENV
          - Otherwise: echo "CRITICAL_ISSUES_FOUND=false" >> \$GITHUB_ENV
          - Always set CRITICAL_ISSUES_FOUND at the end
          PROMPT_EOF

      - name: Check blocking review results
        if: env.BLOCKING_REVIEW == 'true'
        run: |
          echo "Checking for critical issues..."
          echo "CRITICAL_ISSUES_FOUND: ${CRITICAL_ISSUES_FOUND:-unset}"

          if [ "${CRITICAL_ISSUES_FOUND:-false}" = "true" ]; then
            echo "❌ Critical issues found and blocking review is enabled. Failing the workflow."
            exit 1
          else
            echo "βœ… No blocking issues found."
          fi

That’s it! You now have an AI code reviewer that never gets tired, never misses obvious issues, and provides consistent feedback on every PR. ✨