PERMISSION/PROTOCOL
Back to blog

Engineering · April 2, 2026

How to Require Human Approval Before AI Agents Deploy to Production

The Problem With "Trust the Agent"

You've set up an AI agent (maybe Claude Code, Cursor, or a custom LangChain pipeline) to handle code changes. It opens pull requests, passes your CI suite, and merges when tests go green. At some point, someone asks: "But who approved that deploy?"

The honest answer, in most setups today: nobody. The AI agent autonomously decided the action was safe. The CI system checked it against static rules. No human explicitly said "yes, deploy this to production right now."

That's not a workflow problem. It's an architectural problem. And it means any policy you think you have ("engineers must approve all production deploys") is advisory at best. The agent can satisfy the constraint by satisfying its own checks.

The core issue: An AI agent that can satisfy its own authorization check has no authorization check. You need a deploy gate enforced by an authority the agent does not control.

What a Real AI Agent Approval Workflow Looks Like

A proper human approval workflow for AI agents has three components:

1. An external authorization service. Separate from the agent, separate from your CI system. When an AI agent wants to deploy, it must request authorization from this service. The service collects human approval (via Slack, email, a web UI, or an API call from a human) and issues a signed receipt when approved.

2. A cryptographic receipt. The approval is not stored in a database the agent can read. It's issued as an Ed25519-signed receipt scoped to the exact action: this commit SHA, this repo, this environment, valid for this time window. The receipt is tamper-evident and designed for independent verification.

3. A fail-closed enforcer. A GitHub Action (or equivalent) runs at the deploy boundary. It checks whether a valid signed receipt exists for the current deploy scope. No receipt → deploy blocked. Valid receipt → deploy proceeds. Branch protection makes the check required when bypass settings are configured correctly.

This is the Permission Protocol deploy gate pattern. Let's wire it up.

Step 1: Install the GitHub Action

Create .github/workflows/deploy-gate.yml in your repository:

name: Deploy Gate

on:
  pull_request:
    branches:
      - main
      - "release/**"

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

jobs:
  approval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Require signed authority
        uses: permission-protocol/deploy-gate@v1
        with:
          workspace: "${{ vars.PP_WORKSPACE }}"
          protected_paths: |
            .github/workflows/deploy.yml
            infra/**
            migrations/**
        env:
          PERMISSION_PROTOCOL_TOKEN: "${{ secrets.PERMISSION_PROTOCOL_TOKEN }}"

The product app generates the repo-specific workflow during setup. Use the generated workspace value, token, and protected paths for the repo you are testing.

Step 2: Make the Check Required

A GitHub Action that's optional is theater. Go to Settings → Branches → Branch protection rules for main:

  • Enable "Require status checks to pass before merging"
  • Add permission-protocol/approval as a required check
  • Restrict bypass permissions so the wrong people cannot skip required checks

Now PRs targeting main cannot satisfy the protected status check without a valid human approval receipt, assuming branch protection and bypass settings are configured correctly.

Step 3: Issue Authorization from Your SDK

When a human decides to approve a deploy, they call the authorization API. This can be wired into a Slack bot, a web dashboard, or a CLI command:

import { PermissionProtocol } from '@permission-protocol/sdk';

const pp = new PermissionProtocol({ apiKey: process.env.PP_API_KEY });

// Called when a human clicks "Approve" in your dashboard
async function approveDeployRequest(deployRequest) {
  const receipt = await pp.authorize({
    action: 'deploy',
    scope: `${deployRequest.repo}/${deployRequest.sha}`,
    environment: 'production',
    authorizedBy: deployRequest.approverEmail,
    expiresIn: '1h',   // receipt valid for 1 hour
  });

  // receipt.signature is an Ed25519 signature
  // receipt.id can be stored and referenced later
  return receipt;
}

The receipt is cryptographically bound to the specific commit SHA and environment. It expires. It names the human who authorized it. It cannot be reused for a different commit or transferred to a different environment.

Step 4: Wire the AI Agent

Your AI agent's deploy flow changes to include an authorization request before any production action:

async function agentDeployFlow(repo, sha) {
  // 1. Agent prepares the PR
  const pr = await github.createPR({ repo, sha, base: 'main' });

  // 2. Agent requests human approval (does NOT self-authorize)
  const authRequest = await pp.requestAuthorization({
    action: 'deploy',
    scope: `${repo}/${sha}`,
    environment: 'production',
    requestedBy: 'ai-agent',
    notifyChannel: '#deploys',  // Slack channel for human review
  });

  console.log(`Deploy pending human approval. Request ID: ${authRequest.id}`);
  // Agent stops here. It does not merge. It waits.
  // When a human approves, the GitHub check passes automatically.
}

The agent cannot proceed. It cannot self-approve. It cannot satisfy the check by any means other than a human clicking "Approve" in your authorization system.

How the Receipt Verification Works

When the GitHub Action runs, it calls the Permission Protocol API with the current commit SHA. The API looks up whether a valid, unexpired Ed25519-signed receipt exists for that exact scope. If yes, the check passes. If no, it fails with a message like:

✗ Deploy Gate: No valid authorization receipt found
  Scope: myorg/myrepo/abc123def456
  Environment: production
  Required: signed receipt from a human approver
  To authorize this deploy, visit: https://app.permissionprotocol.com/authorize/req_abc123

The check is tied to the exact repo, commit, environment, and approval receipt. Receipts are designed for independent verification; CLI-based local verification is on the roadmap.

What This Doesn't Cover (And What Does)

This pattern gates production deploys triggered by code merges. It doesn't cover:

  • Direct infrastructure changes (Terraform, kubectl) made outside CI
  • Database migrations run by agents with direct DB access
  • Agents that push to branches with weaker protection rules

For those, the same cryptographic receipt pattern applies, the enforcement point just moves from a GitHub Action to a Terraform wrapper, a migration runner hook, or an infrastructure controller. See the quickstart docs for patterns beyond GitHub Actions.

The core principle doesn't change: the AI agent must not be able to authorize itself. The deploy gate must be external. The proof must be cryptographic. The enforcement must be fail-closed.

In practice: Start with one repo, one generated workflow, and one required status check. Once configured, agent-authored production PRs stay blocked until a human explicitly signs the exact action.

The Audit Trail You Actually Want

Every deploy that goes through the gate produces a signed receipt. The receipt includes: who authorized it, what they authorized (exact commit SHA + environment), when the authorization was issued, when it expires, and the Ed25519 signature for independent verification.

This is a fundamentally different artifact than a log entry. A log says "deploy happened at 14:32." A receipt says "Sarah Chen authorized this specific deploy at 14:30, valid until 15:30, cryptographic proof attached." See our pricing page for receipt retention and audit export options by plan.

When your security team asks "who approved the 3 AM deploy?", you have an answer, not a log search, not an assumption, but a cryptographic proof tied to a specific human decision.

Add a human approval workflow to the first repo where agents touch production.

Read the Quickstart →