Skip to content

A 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.

  • 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:

ControlWhat it enforcesHow you test it
IdentityThe agent’s commits, the merger or approver, and the cloud deploy role (assumed through OIDC) are distinct principalsThe cloud audit trail shows the deploy role, never an agent or personal credential
CredentialsNo agent session or general runner holds a production secret; the approved job gets a short-lived oneListing environment variables and secrets from an agent session finds nothing usable
RepositoryEvery change reaches the default branch through a pull request, required checks and a code owner, with no bypassA direct push as the agent identity fails
DeploymentThe production job waits for a named reviewer who did not trigger itSelf-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.

  1. 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 workflows permission, so it cannot change .github/workflows/. Agent identity, credentials and secrets covers the per-agent service accounts and token scopes.

  2. 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.

  3. Create a production environment 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.

  4. Delete standing production secrets. Remove production tokens from repository secrets, developer .env files and agent runners. The deploy job exchanges a GitHub OIDC token for short-lived cloud credentials, and the cloud trust policy accepts only the subject repo: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.

  5. Build the evidence before the approval. A separate job assembles the release evidence and writes it to the job summary, and the production job needs it. The approver reads the evidence on the same run page where the Review deployments button appears.

  6. 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:

.github/workflows/release.yml
name: release
on:
push:
branches: [main]
permissions:
contents: read # every job starts read-only
jobs:
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).

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:

EvidenceWhy the approver needs it
Commit SHA and the linked intent.md, spec.md and plan.mdProves that this revision is the one that was specified and planned
Test, lint, type, security and migration results, split into passed, failed, skipped and manualA skipped check is not a passed check
Review findings and how each one was resolvedShows that Important findings were closed, not ignored
Blast radius, the observability queries to watch, and the rollback command with its last rehearsal dateLets the approver judge the downside, not only the upside
Missing checks and exceptions, each with an ownerMakes a known gap a decision instead of a surprise
Artifact provenance or signatures, where the platform supports themTies 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:main is 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 main that references environment: production is 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.