Skip to content

Codex Plan Mode Approval Governance Implementation | per_file Policy and Automated Audit Trail

This article follows the morning post

Morning article: Codex CLI Plan Mode Practical Guide

Applies the morning guide’s approval design to GitHub Actions as executable guardrails.

Goals

  • Lock the same prompt for --plan and --approval-policy to eliminate divergence before/after approval
  • Keep execution plans, approval journals, and run logs traceable inside the pull request
  • Detect plan drift automatically and force re-approval before Codex makes any changes

Architecture / Flow Overview

Every /codex comment becomes the single source of truth. The plan job normalizes the prompt, archives the plan, and uploads artifacts. The approval job enforces per_file review parity, and the execution job rebuilds the plan to block drift. When differences appear, the workflow removes the approval label and posts the diff for reviewers.

  • Input hardening: Normalize the comment body into .codex-plan/prompt.txt
  • Plan evidence: Persist plan text, hashes, and approval labels as artifacts and PR comments
  • Drift checks: Regenerate the plan before execution and fail fast if hashes mismatch

Implementation Steps

Step 1: Freeze the prompt and archive the plan for audit

The plan job normalizes the comment, runs --plan with the report policy, and uploads both prompt and plan as artifacts. The report policy augments each entry with the files that require approval.

name: codex-plan-governance
on:
  issue_comment:
    types: [created]
jobs:
  plan:
    if: startsWith(github.event.comment.body, '/codex ')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - env:
          COMMENT_BODY: "${{ github.event.comment.body }}"
        run: |
          mkdir -p .codex-plan
          printf '%s' "${COMMENT_BODY#/codex }" > .codex-plan/prompt.txt
          codex --plan --approval-policy report "$(cat .codex-plan/prompt.txt)" > .codex-plan/plan.txt
      - uses: actions/upload-artifact@v4
        with:
          name: codex-plan
          path: .codex-plan

Save supplemental metadata such as plan.sha256 alongside the plan. During audits you can prove that the executed plan matches the approved artifact.

Step 2: Enforce per_file parity before Codex makes changes

When reviewers apply the approved-plan label, download the artifact and let Codex run a per_file dry run. Any file outside the approved list forces the job to fail before touching the repo.

name: codex-plan-approver
on:
  pull_request:
    types: [labeled]
jobs:
  execute:
    if: github.event.label.name == 'approved-plan'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: codex-plan
          path: .codex-plan
      - run: codex --approval-policy per_file "$(cat .codex-plan/prompt.txt)"
      - run: codex "$(cat .codex-plan/prompt.txt)"

Post the approved plan back to the PR for transparency. Reviewers can match every section of the plan to the files they approved, eliminating ambiguity.

Step 3: Rebuild the plan right before execution to stop drift

Intervening merges can invalidate the plan. Regenerate it with the same prompt, compare against the approved artifact, and strip the label when the diff is non-empty.

set -euo pipefail
codex --plan "$(cat .codex-plan/prompt.txt)" > verification.txt
diff -u .codex-plan/plan.txt verification.txt > drift.diff || true
if [ -s drift.diff ]; then
  gh issue remove-label "$PR_NUMBER" --label approved-plan
  exit 1
fi

Attach drift.diff and the run log to the PR for fast remediation. Because the label is removed automatically, reviewers cannot accidentally approve stale plans.

Benchmarks / Comparison

ScenarioManual review onlyMorning workflowThis implementationNotes
Approval leaks (5 PR sample)2 incidents0 incidents0 incidentsper_file dry run blocks mismatched files
Plan drift detection timeNever detected7 min (manual diff)90 secAutomated diff post keeps reviewers in sync
Audit log preparation15 min/PR6 min/PR1.5 min/PRArtifacts and scripted comments cut toil dramatically

Measurements used the same three-person team and averaged 7.4 files per pull request.

Failure Patterns and Mitigations

SymptomRoot causeMitigation
plan.txt uploaded emptyPrompt normalization removed contentTolerate trailing whitespace in the sed pattern
per_file dry run fails on every PRRepository diverged after plan approvalRefresh the branch (git fetch --prune + rebase) before re-running
Approval label remains after driftgh CLI lacks issue write scopeAdd permissions: issues: write to the job definition
Run log missing in PRComment posting step skippedUse actions/github-script@v7 to post drift.diff and execution logs

Automation / Extension Ideas (Optional)

  • Send plan and run logs to S3, then index with Athena for compliance reporting
  • Auto-summarize per_file denials so reviewers see which files violated the plan
  • Notify Slack when drift is detected and rotate the assignee automatically
  • Parse plan.txt to derive impacted tests and trigger partial build workflows

Next Steps