Production approval as a system boundary
PLA production approval gate stops coding agents from deploying unvetted changes by putting the decision where no agent session can reach it: separate agent and deploy identities, no standing production credentials in any agent environment, protected branches with no bypass, and a deployment environment that a named human approves. Prompts and hooks only catch mistakes early.
Your agents open pull requests all day, and one of them just ran deploy.sh production in a terminal because the token was in .env and the prompt said “finish the task”. Nothing broke this time. This page is for the developer who owns the release pipeline and the tech lead who has to answer “what actually stops an agent from shipping to production?” with something better than “we told it not to”.
Scorecard question (Q20): What controls stop an agent from deploying unvetted changes directly to production?
Maximum-score answer: The agent environment has no standing production credentials; protected branches and a restricted CI environment require named human approval and preserve an audit trail.
What a production approval gate gives you
Section titled “What a production approval gate gives you”- A GitHub setup, in six steps, in which the only path to production is one approved job on one revision.
- A release workflow that builds the evidence first and hands out cloud credentials only after approval.
- Session-side containment for Claude Code, Codex and Cursor that keeps production out of reach before CI is involved.
- Three copy-paste prompts: a readiness report, a skeptical release review, and an audit of the gate itself.
- Seven boundary tests you run as the agent’s own identity, so you prove the gate holds instead of trusting the settings page.
Why is a prompt, a hook or an environment variable not the gate?
Section titled “Why is a prompt, a hook or an environment variable not the gate?”An agent can edit, reword or route around anything that lives in its own session. RELEASE_APPROVAL=true is a string any process can set, a system prompt is advice, and a hook matches command text: sh -c '…', a wrapper script or a different client slips past it. Those controls are useful as early warnings. The boundary has to be four controls that the agent’s identity cannot change:
| Control | What it enforces | How you test it |
|---|---|---|
| Identity | The agent’s commits, the merger or approver, and the cloud deploy role (assumed through OIDC) are distinct principals | The cloud audit trail shows the deploy role, never an agent or personal credential |
| Credentials | No agent session or general runner holds a production secret; the approved job gets a short-lived one | Listing environment variables and secrets from an agent session finds nothing usable |
| Repository | Every change reaches the default branch through a pull request, required checks and a code owner, with no bypass | A direct push as the agent identity fails |
| Deployment | The production job waits for a named reviewer who did not trigger it | Self-approval fails, and the approval names one commit SHA |
The Deploy stage places this gate in the lifecycle. This page is the detailed setup.
How do you set up the production gate on GitHub?
Section titled “How do you set up the production gate on GitHub?”The steps use GitHub, because its environment rules and OIDC claims are what we verified (GitHub docs, checked 2026-09-26). Other forges and deploy platforms need the same four controls under their own names.
-
Give the agent its own identity. Run agents as a GitHub App or a dedicated bot account with write access to branches and pull requests only, never as an administrator and never with a personal token of the engineer who launched them. A GitHub App gets contents and pull-request write permission but never the
workflowspermission, so it cannot change.github/workflows/. Agent identity, credentials and secrets covers the per-agent service accounts and token scopes. -
Protect the default branch with a ruleset. In Settings › Rules › Rulesets, target the default branch and turn on: require a pull request, require review from code owners, dismiss stale approvals when new commits are pushed (or require approval from someone other than the last pusher), and required status checks with the expected source app pinned. Leave the bypass list empty. An agent app on the bypass list defeats every other rule.
-
Create a
productionenvironment with required reviewers. In Settings › Environments, add up to six users or teams as required reviewers (one approval lets the job proceed), turn on Prevent self-reviews, and restrict deployment branches to the default branch. By default, administrators can bypass environment protection rules, so switch that off. On GitHub Free, Pro and Team plans, required reviewers work only in public repositories; a private repository needs GitHub Enterprise or an equivalent gate in your deploy platform. -
Delete standing production secrets. Remove production tokens from repository secrets, developer
.envfiles and agent runners. The deploy job exchanges a GitHub OIDC token for short-lived cloud credentials, and the cloud trust policy accepts only the subjectrepo:ORG/REPO:environment:production. A job that does not reference the environment, including any job an agent triggers from a branch, gets a token with a different subject, and the cloud refuses it. -
Build the evidence before the approval. A separate job assembles the release evidence and writes it to the job summary, and the production job
needsit. The approver reads the evidence on the same run page where the Review deployments button appears. -
Record the decision. The run’s approval history is queryable (
GET /repos/OWNER/REPO/actions/runs/RUN_ID/approvals) and names the reviewer, the time and the comment. Keep it with the deployment record so the auditor can join author, reviewer, approver, deployer and result on one SHA.
The release workflow that implements steps 4 and 5:
name: releaseon: push: branches: [main]permissions: contents: read # every job starts read-onlyjobs: evidence: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: persist-credentials: false - run: ./scripts/release-evidence.sh "$GITHUB_SHA" > release-evidence.md - run: cat release-evidence.md >> "$GITHUB_STEP_SUMMARY" - uses: actions/upload-artifact@v7 with: name: release-evidence path: release-evidence.md
deploy-production: needs: evidence runs-on: ubuntu-latest environment: production # waits here for a required reviewer concurrency: group: production cancel-in-progress: false permissions: contents: read id-token: write # OIDC, issued only after approval steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ vars.PROD_DEPLOY_ROLE_ARN }} # trust policy: sub = repo:ORG/REPO:environment:production aws-region: eu-central-1 - run: ./scripts/deploy.sh "$GITHUB_SHA"The concurrency group keeps one production deployment in flight at a time. An approval names one SHA, so when a newer push supersedes a pending run, reject the older one instead of approving it.
PROD_DEPLOY_ROLE_ARN is a variable, not a secret: the role name grants nothing without the OIDC token. Replace the AWS step with your cloud’s OIDC login action; the subject condition is what matters. No agent runs inside this workflow. If you add an agent step, for example to draft release notes, put it in the evidence job with no write token, as AI in CI/CD describes.
How do you keep the agent session away from production?
Section titled “How do you keep the agent session away from production?”CI holds the gate. The session controls stop an agent from reaching production before CI is involved, and they differ by tool. In all three, work in an isolated worktree wired to local or ephemeral services, never to a checkout that can reach live data. Permissions and sandboxes compares the controls in full.
Deny the deploy commands and hide the credentials in .claude/settings.json, or in managed settings so an engineer cannot remove them:
{ "permissions": { "deny": ["Bash(./scripts/deploy.sh *)", "Bash(terraform apply *)", "Read(./.env.production)"] }}Add the PreToolUse hook from the Deploy stage so a refused deploy tells the agent which route works. The sandbox’s sandbox.credentials setting can deny or mask credential files and variables, and nothing is protected by default. For review jobs in CI, claude -p --restricted --strict-mcp-config removes the tools that run commands, ignores project settings and loads no MCP server from the repository’s .mcp.json; --restricted alone still starts those servers (checked against claude --help, 2.1.283).
Run implementation work with a permission profile (-c default_permissions=":workspace", beta) or the legacy --sandbox workspace-write, and keep approvals on with -a on-request. Do not combine a profile with --sandbox: OpenAI says the two systems do not compose. --full-auto no longer exists (absent from codex --help 0.157.1). An administrator can pin permission_profile and managed hooks in requirements.toml. Never use --dangerously-bypass-approvals-and-sandbox outside a disposable, network-restricted sandbox, and never in a shell that holds a production credential.
Cursor documents approval and sandbox behaviour on its Run modes page under Agent security, and Hooks can block a shell command before it runs. The run modes are Auto-review (the recommended default since Cursor 3.6), Allowlist and Run Everything, under Settings > Agents > Approvals & Execution (Cursor docs, checked 2026-10-12); Cloud Agents do not use them, because they run on their own dedicated machine. Read the CLI flags from your own build before you set a team standard. On the pull-request side, Rollouts and PR Routing & Approval routes approvals by risk, but it approves pull requests, not production jobs: keep the environment gate above.
What should the approver see before clicking approve?
Section titled “What should the approver see before clicking approve?”The approver decides on evidence, not on a diff. The job summary carries the release section of the evidence bundle:
| Evidence | Why the approver needs it |
|---|---|
Commit SHA and the linked intent.md, spec.md and plan.md | Proves that this revision is the one that was specified and planned |
| Test, lint, type, security and migration results, split into passed, failed, skipped and manual | A skipped check is not a passed check |
| Review findings and how each one was resolved | Shows that Important findings were closed, not ignored |
| Blast radius, the observability queries to watch, and the rollback command with its last rehearsal date | Lets the approver judge the downside, not only the upside |
| Missing checks and exceptions, each with an owner | Makes a known gap a decision instead of a surprise |
| Artifact provenance or signatures, where the platform supports them | Ties the deployed artifact to the reviewed commit |
Rejection must be as cheap as approval. The approver rejects in the platform, and the agent picks up the reason from the run, never from a channel that would hand it credentials.
Run the next prompt in a read-only agent session, or in the evidence job with no write token, to draft the report that scripts/release-evidence.sh publishes.
How do you prove the gate holds without reading every workflow?
Section titled “How do you prove the gate holds without reading every workflow?”Settings pages describe intent. Tests run as the real identities prove behaviour. Run these after setup, after any change to the rules, and on a schedule:
- As the agent identity,
git push origin HEAD:mainis rejected. - As the agent identity, pushing a change under
.github/workflows/is rejected. - As the agent identity, approving a pending deployment on a run it triggered fails.
- From an agent session,
env, the secrets store and the runner filesystem show no production credential. - A branch other than
mainthat referencesenvironment: productionis refused by the deployment branch policy. - The approval history for the last release names one human reviewer and one commit SHA.
- A rollback runs through the same protected path without granting the implementation agent production access.
Turn the settings checks into drift detection: a scheduled job that runs gh api repos/OWNER/REPO/environments/production and fails when prevent_self_review is false, the reviewer list is empty, or the branch policy allows every branch. The rollback pipeline covers the rehearsal side.
What breaks in a production gate, and how do you recover?
Section titled “What breaks in a production gate, and how do you recover?”The hook is the only barrier. A deploy runs through sh -c and nothing else stops it. Recovery: remove the production credential from every agent environment first, then enforce the gate in the forge and the deploy platform. Keep the hook as an early warning.
The agent’s pull request edits the gate. It drops the needs: evidence dependency from release.yml or weakens scripts/release-evidence.sh, and a generic code-owner review passes it without anyone noticing that the gate changed. Recovery: put .github/workflows/, scripts/deploy.sh and scripts/release-evidence.sh in CODEOWNERS under the release owner, and add a boundary test that a workflow change pushed as the agent identity is rejected.
The agent app sits on a bypass list. Someone added it “to unblock automation”, and every rule became optional. Recovery: empty the ruleset bypass list, rerun the direct-push test as the agent identity, and give automation its own pull-request path.
Administrators can bypass the environment. The default allows it, and an agent running under an admin’s token inherits it. Recovery: disable admin bypass on production, and never run agents with an administrator’s credentials.
Approval happens on an incomplete evidence page. The reviewer approves because the button is there. Recovery: make the evidence job a needs dependency that fails when any required section is empty, so the deployment is never offered without it.
A long-lived deploy token survives in a repository secret. A workflow on another branch reads it and bypasses the environment. Recovery: rotate the token, delete the repository secret, and move to OIDC with the environment subject condition.
Break-glass access becomes permanent. An emergency grant from an incident is still active months later. Recovery: issue break-glass access with an expiry, log each use, and review it in the incident postmortem, as When an agent causes an incident describes.
Where to go next after the production gate
Section titled “Where to go next after the production gate”Official references: GitHub deployment environments, GitHub OpenID Connect, available ruleset rules, and Claude Code hooks.
Frequently asked questions
What controls prevent a coding agent from deploying unvetted changes to production?
A production approval gate enforces a hard system boundary: separate agent and deploy identities, zero standing production credentials in agent environments, protected branches with no bypass, and a deployment job that requires named human approval.
Are prompts and local hooks sufficient as a production deployment gate?
No, prompts and local hooks only catch mistakes early inside the agent session; true production protection requires identity separation and protected deployment environments outside the agent reach.