← Back to blog
Use CasesCI11 min read

September 3, 2026

Deny deploy on a Copilot PR burst before production credentials

Copilot opened twelve pull requests in twenty minutes: import cleanup, build-cache tweak, fixture rename, mechanical TypeScript edits. Each PR triggered the same workflow. A pull_request_target job shared a path with aws-actions/configure-cloud-credentials: checkout of PR code for tests, then a deploy step that assumed production credentials only minted on trusted code.

GitHub withholds secrets from fork pull_request runs. That was not this hole. pull_request_target runs in the base-repo context. Untrusted checkout and credential minting sat in one job. No attacker opened a PR — a coding agent did, twelve times.

Gate volume from helpful loops

The failure mode is cloud credentials in the wrong execution context. Branch protection and environment reviewers still matter; they run later. They do not classify pull_request_target as untrusted for deploy, and they do not write an evaluate row you can query after the burst.

Target: 12 PRs → 12 test intents allow, 12 deploy intents deny before credentials load.

Split policies and evaluate before production credentials

@limetry/ci uses two lanes:

  1. ci-privilege.json allows ci_privilege for tests and denies deploy.
  2. deploy.json allows deploy, but the job still requires a trusted GitHub event.

Classify the event, then evaluate. Trusted: push, release, workflow_dispatch. Untrusted: pull_request, pull_request_target, review events, issues, workflow_run, refs/pull/*. Unknown future events — including schedule — are untrusted by default.

Evaluate still runs on the untrusted path so deny is a recorded decision. require_trusted: true fails the step after evaluate when trust is untrusted, so configure-cloud-credentials never runs.

import { evaluateCiPrivilege } from "@limetry/ci"

const deployResult = await evaluateCiPrivilege({
  apiKey: process.env.LIMETRY_API_KEY!,
  baseUrl: process.env.LIMETRY_BASE_URL,
  policyId: process.env.LIMETRY_DEPLOY_POLICY_ID!,
  agentId: "github_actions",
  actionType: "deploy",
  repository: "acme/api",
  sha: process.env.GITHUB_SHA!,
  eventName: process.env.GITHUB_EVENT_NAME!,
  ref: process.env.GITHUB_REF,
})

if (deployResult.trust === "untrusted" || deployResult.evaluation.decision !== "allow") {
  throw new Error(
    [
      "Limetry denied deploy",
      `trust=${deployResult.trust}`,
      `decision=${deployResult.evaluation.decision ?? "deny"}`,
    ].join(" "),
  )
}

Place this step before credential loading. GitHub environment rules still apply after. Ask first: should this action type run from this event at this SHA?

Deny on the PR; park approval on main

Typical PR deploy receipt against ci-privilege.json:

{
  "intent": {
    "agent_id": "github_actions",
    "action_type": "deploy",
    "resource": "acme/api@9f4c2b1",
    "metadata": {
      "event_name": "pull_request_target",
      "trust": "untrusted"
    }
  },
  "decision": "deny",
  "reasons": [
    "action_type deploy is denied"
  ]
}

Test lane: allow for ci_privilege. Deploy lane: deny for every PR. Trust stays untrusted. Tests keep running. The job never reaches configure-cloud-credentials.

After merge, main-branch deploy under the deploy policy may return approval_required. Fail closed with an approval_id. Approve via the approval API (operator approval). Retry until allow with a signed receipt (digest, exp, sig) that verifyReleaseReceipt can bind to the intent. Only then run configure-cloud-credentials.

Results from the burst

  • 12 PRs evaluated in less than one second each (self-run evaluate).
  • 12 test lanes returned allow.
  • 12 deploy lanes returned deny before production credentials loaded.
  • 1 main-branch deploy parked as approval_required until a team lead approved.

Audit answers which agent asked for deploy, from which SHA, on which event, and why policy stopped it. The release still shipped. Untrusted PRs never touched credentials.

Origin: Evaluate before CI loads credentials. Rollout: Staging the CI gate.

Run Limetry on your own stack

Self-host the open source evaluation server, wire evaluate into your agents, and keep privacy-safe audit under your control.