GitHub Actions PR Gate for AI-Generated Code

AI-generated code needs a pull-request gate.

Not because AI code is always bad. Because AI tools make it cheap to produce large, plausible diffs that touch code, tests, dependencies, and workflow files at the same time.

A reviewer can miss the important part:

  • an auth guard removed during a route cleanup
  • a tenant filter dropped from a query
  • a validation schema replaced with a looser type
  • a new package added for a tiny helper function
  • a GitHub Actions workflow changed to run with broader permissions
  • a realistic-looking secret committed as an example

The right answer is not "ban AI-generated code." The right answer is a GitHub Actions PR gate for AI-generated code that treats AI-assisted PRs like any other high-speed contributor: useful, but not trusted until checked.

This guide gives you a practical workflow.


The short version

Use this model:

LayerPurposeShould it block?
Local scanCatch obvious issues before the PR existsDeveloper choice
GitHub Actions PR gateCheck changed code, secrets, CI YAML, dependencies, and removed controlsYes, for high-confidence findings
Human reviewReview product behavior, architecture, and intentYes
Optional cloud historyTrack trends, exceptions, evidence, and repeated findings over timeTeam choice

The most important GitHub Actions rule is simple:

Do not run untrusted PR code in a privileged workflow.

For most AI-generated PRs, use pull_request, not pull_request_target.

GitHub's own hardening guidance warns that pull_request_target and workflow_run can expose the repository when they are used with untrusted pull request code. GitHub also documents that values from the github context, including pull request titles, bodies, branch names, and similar event fields, should be treated as untrusted input because they can lead to script injection.

Sources:


What makes AI-generated PRs different?

Traditional CI usually asks:

Does the code build and do tests pass?

That is not enough for AI-assisted changes.

AI-generated pull requests often fail in ways that build systems do not see:

Failure modeWhy tests miss it
Removed auth checkHappy-path tests still pass for valid users
Removed tenant filterSingle-tenant fixtures do not expose cross-tenant reads
Weaker validationValid sample payload still passes
Missing timeoutUnit tests use fast local mocks
Hallucinated helperThe name looks project-native but has no real implementation
New dependencyLockfile changed, but nobody reviewed install scripts or package trust
Workflow permission changeApp tests pass while CI token scope gets wider

So the PR gate needs to inspect both:

  1. What the new code contains
  2. What the old code used to protect

That second part is the AI-specific failure mode.


The safe baseline workflow

This is the shape of a safer AI PR security gate in GitHub Actions:

name: AI PR security gate

on:
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  skylos:
    name: Scan changed code
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Skylos
        run: python -m pip install --upgrade skylos

      - name: Scan changed code
        run: |
          skylos . -a --diff "origin/${{ github.base_ref }}" --json > skylos-report.json

      - name: Annotate findings
        if: always()
        run: |
          skylos cicd annotate --input skylos-report.json --max 50

      - name: Enforce gate
        run: |
          skylos cicd gate --input skylos-report.json --summary

      - name: Upload report artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: skylos-report
          path: skylos-report.json
          if-no-files-found: error
          retention-days: 30

That workflow is intentionally boring.

It runs on pull_request. It grants contents: read. It does not access production secrets. It does not deploy. It does not ask a model to decide whether the PR is safe. It runs deterministic checks and fails the gate when the findings are strong enough.

You can also generate a starter workflow from the CLI:

pip install skylos
skylos cicd init

If you want uploaded scan history in Skylos Cloud later:

skylos cicd init --upload

The local-first path matters. Run the scanner on one repository first. Do not design a company-wide workflow before you know which findings are useful on your codebase.


Why not use pull_request_target?

pull_request_target is not automatically wrong. It is sharp.

It runs in the context of the base repository, not the pull request branch. That can be useful when you need to label a PR, comment on it, or run trusted automation that does not execute the proposed code.

It becomes dangerous when you combine it with untrusted code execution.

Bad pattern:

on:
  pull_request_target:

permissions:
  contents: write
  pull-requests: write

jobs:
  unsafe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}

      - run: npm install
      - run: npm test

This pattern checks out and runs pull request code inside a privileged workflow context. A malicious or compromised branch can modify package scripts, test commands, build tools, or repository config so that the privileged workflow executes attacker-controlled code.

Safer pattern:

  • use pull_request for untrusted code execution
  • keep contents: read unless you truly need more
  • separate "test untrusted code" from "write comments or labels"
  • do not pass repository secrets into untrusted PR jobs

If you need comments, prefer GitHub annotations first. If you need full PR review comments, scope permissions narrowly:

permissions:
  contents: read
  pull-requests: write

Only add that when the job really posts comments.


What the AI PR gate should check

1. Removed security controls

This is the highest-value check for AI-assisted PRs.

Look for deleted or weakened:

  • auth middleware
  • role checks
  • tenant filters
  • ownership checks
  • validation schemas
  • rate limits
  • CSRF protection
  • CORS restrictions
  • audit logs
  • timeout handling

Example:

- user: User = Depends(require_user)
  db: Session = Depends(get_db)

  return (
    db.query(Customer)
    .filter(Customer.id == customer_id)
-   .filter(Customer.tenant_id == user.tenant_id)
    .one()
  )

The route still works. The test may still pass. The tenant boundary is gone.

Your PR gate should make that kind of change visible before review.

2. Secrets and realistic placeholders

AI tools often generate realistic-looking examples:

OPENAI_API_KEY = "sk-proj-..."
STRIPE_WEBHOOK_SECRET = "whsec_..."
DATABASE_URL = "postgres://admin:password@localhost:5432/app"

The gate should block committed tokens, high-entropy strings, private keys, and suspicious credentials even when they appear in tests or examples.

3. Dependency changes

AI agents like adding packages.

Sometimes that is fine. Sometimes it is a new supply-chain risk for a helper function that should have been ten lines of local code.

Review:

  • new package names
  • version bumps
  • lockfile changes
  • install scripts
  • new transitive dependency size
  • packages added only by generated code

The gate should not blindly reject every new dependency. It should force the dependency change to be seen.

4. GitHub Actions workflow changes

Workflow files are privileged code. AI-generated changes to .github/workflows/*.yml deserve the same review as application code.

Block or review:

  • pull_request_target added to a workflow
  • broad permissions: write-all
  • missing permissions block
  • unpinned third-party actions
  • persist-credentials: true when not needed
  • id-token: write in jobs that run repo-controlled scripts
  • secrets used in PR-triggered jobs
  • untrusted GitHub context expanded directly into run:

GitHub's script-injection guidance is especially relevant for AI-generated workflow edits. Pull request titles, branch names, issue bodies, and similar fields are attacker-controlled input.

Risky:

- name: Check PR title
  run: |
    title="${{ github.event.pull_request.title }}"
    echo "Checking $title"

Better:

- name: Check PR title
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: |
    python scripts/check_pr_title.py "$PR_TITLE"

Using an environment variable does not magically make everything safe, but it avoids directly splicing untrusted expression output into a shell script.

5. Generated tests that only prove the happy path

AI-generated tests often confirm that the new code works for the ideal case.

For security-sensitive routes, require negative tests:

  • unauthenticated request fails
  • unauthorized role fails
  • cross-tenant access fails
  • invalid input fails
  • rate-limited request fails
  • export without permission fails
  • revoked token fails

If the PR touches auth, billing, exports, admin actions, invitation flow, webhooks, secrets, or integrations, happy-path tests are not enough.


Local-first version before CI

Before putting a new gate into branch protection, run it locally on a few real branches.

pip install skylos
skylos . -a --diff origin/main

If the signal is too noisy, tune thresholds and ignores before making the gate blocking.

Then generate CI:

skylos cicd init

Then make the gate required only after the team trusts the findings.

That rollout sequence matters:

  1. local scan
  2. advisory CI
  3. blocking CI for high-confidence findings
  4. optional Cloud history for teams that need trends, exceptions, and evidence

Do not start with a blocking company-wide rollout if you have not reviewed the findings on one repo.


What to block vs what to warn on

Not every finding should fail the PR.

Use this split:

FindingGate behavior
Committed secretBlock
Removed auth, tenant scoping, validation, or rate limitBlock
Dangerous GitHub Actions trigger or broad write permissionBlock or require security review
New vulnerable dependencyBlock
New high-confidence injection, SSRF, path traversal, command executionBlock
Dead code in changed filesWarn first, then block if the team agrees
Complexity or maintainability debtWarn or track
Low-confidence style issueDo not block

The goal is not to make CI angry. The goal is to prevent expensive misses while keeping developer trust.


Where Skylos fits

Skylos is useful here because the PR gate needs more than one scanner category.

For AI-generated code, you want one workflow that can check:

  • security findings
  • secrets
  • dead code
  • technical debt hotspots
  • CI workflow risk
  • hallucinated imports or AI-code mistakes
  • changed-code regressions
  • removed controls

Run it locally:

skylos . -a --diff origin/main

Use it in CI:

skylos cicd init

Use Cloud later only when the team needs history, compare, exceptions, exports, or PR evidence.

Skylos should not replace human review. It should remove the machine-checkable risks from the reviewer's plate before the PR reaches main.


Final checklist

Before you let AI-generated PRs merge, make sure your GitHub Actions gate does this:

  • runs on pull_request for untrusted PR code
  • grants contents: read by default
  • avoids repository secrets in untrusted PR jobs
  • avoids pull_request_target unless no untrusted code is executed
  • pins important third-party actions where practical
  • disables persisted checkout credentials unless needed
  • scans changed code, not only the full final tree
  • scans secrets and dependency changes
  • scans workflow files as privileged code
  • blocks removed auth, tenant scoping, validation, rate limits, and dangerous sinks
  • keeps low-confidence findings advisory
  • requires human approval for security-sensitive changes

The practical rule is simple:

Let AI write code. Do not let AI bypass the merge gate.