Skip to content

Plan policy: accepted artifacts before risky execution

A plan policy for AI coding agents requires accepted intent.md, spec.md, and plan.md files before an agent changes sensitive code such as auth, billing, migrations, or infrastructure. Acceptance is a code-owner-approved merge, and a CI check blocks any pull request that touches protected paths without it. Plan mode helps write the plan; it does not enforce the policy.

This page is for the CTO who owns question 17 of the CTO Scorecard and for the tech lead who has to make it work. The usual situation: the team agreed that “risky changes start with a plan”, everyone uses Plan mode, and last month an agent still shipped a schema migration whose plan nobody approved, because the engineer left Plan mode after two minutes and the reviewer saw only the diff.

Q17 · Organization enablement: Do sensitive changes require accepted intent, specification, and plan artifacts before execution?

Max-score answer: accepted intent, spec, and plan are required before write access; a hook or CI verifies the artifacts; and a named approver controls risky actions.

  • A trigger table that decides which changes need an accepted plan, by path and by risk, not by line count
  • A policy file you can commit as-is and adapt in one review
  • A plan-first workflow for Claude Code, Codex, and Cursor that produces the three artifacts
  • A tested CI script and workflow that fail a pull request touching protected paths without accepted artifacts
  • Three copy-paste prompts: draft the plan, attack the plan, and compare the final diff with the plan
  • A self-test that proves the gate works without anyone reading the implementation

The artifact contents come from the artifact chain templates, and the stage itself is described on the Plan stage page. This page covers only the organizational gate around them.

All three agents can plan before they edit. None of them records, in your repository, who accepted which plan, and every one of them lets the person at the keyboard move on to execution. That is the gap Q17 measures.

ToolPlan-first feature (checked 2026-09-26 unless noted)What it enforcesWhat it cannot prove
Claude Code 2.1.283plan permission mode: reads and research, edits blocked until you approve the plan. Enter with /plan, Shift+Tab, or claude --permission-mode planNo file edits during the planning sessionThat anyone other than the session owner accepted the plan. On the latest channel most interactive sessions start in auto mode, not Plan mode, unless you choose otherwise
Codex CLI 0.157.1/plan in the interactive session; the legacy --sandbox read-only or the :read-only permission profile (beta) for any runNo writes while read-onlycodex exec has no plan flag (checked codex exec --help, 0.157.1), so unattended runs need a read-only sandbox or permission profile and the gate, not a mode
CursorPlan Mode “creates detailed implementation plans before writing any code” (Cursor docs, checked 2026-08-28)Planning before code in that conversationAcceptance, approver identity, or a link from the plan to the merged diff

So the policy treats Plan mode as an authoring aid and moves the control to places the agent cannot redefine: the default branch, CODEOWNERS, branch protection, and a required CI check. This is the same split between advice and authority that the governance and autonomy policy applies to every agent control.

Trigger on what a change can break, not on how big it is. A three-line change to a token check is riskier than a 400-line copy update. Start from this table and map each row to real paths in your repository.

TriggerExample pathsWhy a plan is mandatoryNamed approver
Schema or data migrationdb/migrations/Hard to reverse; order of deploy and backfill mattersData platform code owner
Authentication, authorization, sessionssrc/auth/A wrong default locks users out or lets the wrong ones inSecurity code owner
Money: billing, pricing, entitlementssrc/billing/Silent errors cost revenue or refundsPayments code owner
Infrastructure, CI, and deploy configinfra/, .github/Changes what “green” and “deployed” mean for every other changePlatform code owner
Public API contractsapi/openapi.yamlExternal consumers break without a failing test hereAPI owner
Sensitive data flowsfiles that read or export personal dataRegulatory exposurePrivacy or security owner

Anything outside the table follows the normal evidence bundle route. The table is also where the high risk class starts: a change that needs an accepted plan is almost always high in the bundle.

Write access here means write access to main: agents may draft on a branch, but nothing touching a protected path reaches main without an accepted plan.

Commit the policy next to the code it governs, so every agent session and every reviewer reads the same text. Edit the paths and teams, then merge it through the same code-owner review it describes.

docs/policies/plan-gate.md
# Plan gate policy
Owner: VP Engineering. Reviewed: quarterly. Version: 1.
## Scope
A change needs accepted artifacts when it touches a protected path:
db/migrations/, src/auth/, src/billing/, infra/, .github/, api/openapi.yaml,
scripts/check-plan-gate.sh.
## Accepted artifacts
Each protected change has a directory changes/SLUG/ with intent.md, spec.md
and plan.md (templates: docs/templates/). The artifacts are accepted when a pull
request that adds them is approved by the code owner of the affected area and
merged to main. Main is the only record of acceptance.
## Implementation
The implementation pull request names its directory with a line
"Change-Dir: changes/SLUG" in its description. It must not edit the accepted
artifacts. A material deviation (new file outside plan.md, changed data shape,
changed rollout or rollback) stops the work: amend the plan in its own pull
request first. A Change-Dir serves one implementation pull request, and every
protected path that pull request touches must appear in the file list of plan.md.
## Enforcement
The plan-gate CI check is required on main. Code-owner review is required for
changes/, the protected paths, and the gate itself. Write access means write
access to main: agents may draft on a branch, but nothing that touches a
protected path reaches main without an accepted plan. Agents never hold merge
rights or production credentials.
## Exceptions
Emergency fixes use the audited ruleset bypass, are named in the incident
record, and get a retroactive plan pull request within two business days.
The platform team reports every bypass in the monthly engineering review.

The gate has three parts: ownership in CODEOWNERS, a check script, and a required workflow. Add them in one pull request, merge it, and only then mark the check as required. The order matters, because the workflow below runs the copy of the script that is already on the default branch.

  1. Assign owners. Add the change directory, each protected path, and the gate itself to .github/CODEOWNERS. Then turn on Require review from Code Owners in the branch protection or ruleset for main.

    # .github/CODEOWNERS
    /changes/ @acme/architecture
    /db/migrations/ @acme/data-platform
    /src/auth/ @acme/security
    /src/billing/ @acme/payments
    /infra/ @acme/platform
    /.github/ @acme/platform
    /api/openapi.yaml @acme/api
    /scripts/check-plan-gate.sh @acme/platform

    Replace the @acme/... teams with your own. If one architecture group is too coarse for /changes/, give change directories a domain prefix, for example changes/billing-SLUG/, and add /changes/billing-*/ @acme/payments below the /changes/ line. A pattern that ends in / covers the directory and everything nested in it, and in CODEOWNERS the last matching line wins, so the domain line must come after the general one.

  2. Add the check script. It passes pull requests that touch no protected path. For the rest, it requires a Change-Dir: line, requires the three artifacts to exist on the base branch already, and rejects edits to them in the same pull request.

    #!/usr/bin/env bash
    # scripts/check-plan-gate.sh: fail a PR that changes protected paths without accepted artifacts
    set -euo pipefail
    BASE="origin/${GITHUB_BASE_REF:?run on a pull_request event}"
    PROTECTED='^(db/migrations/|src/auth/|src/billing/|infra/|\.github/|api/openapi\.yaml$|scripts/check-plan-gate\.sh$)'
    changed="$(git diff --name-only "$BASE"...HEAD)"
    hits="$(grep -E "$PROTECTED" <<<"$changed" || true)"
    if [ -z "$hits" ]; then
    echo "plan-gate: no protected paths changed"
    exit 0
    fi
    printf 'plan-gate: protected paths changed:\n%s\n' "$hits"
    dir="$(printf '%s\n' "${PR_BODY:-}" | tr -d '\r' \
    | sed -nE 's#^Change-Dir:[[:space:]]*(changes/[A-Za-z0-9._-]+)/?[[:space:]]*$#\1#p' | head -n1)"
    if [ -z "$dir" ]; then
    echo "::error::Add a line 'Change-Dir: changes/SLUG' to the PR description."
    exit 1
    fi
    for f in intent.md spec.md plan.md; do
    if ! git cat-file -e "$BASE:$dir/$f" 2>/dev/null; then
    echo "::error::$dir/$f is not on $BASE. Merge the plan PR, approved by a code owner, first."
    exit 1
    fi
    done
    if grep -q "^$dir/" <<<"$changed"; then
    echo "::error::This PR edits the accepted artifacts in $dir. Amend them in a separate plan PR."
    exit 1
    fi
    echo "plan-gate: $dir accepted at $(git log -1 --format=%h "$BASE" -- "$dir")"

    Keep PROTECTED identical to the scope in the policy file. The last line prints the commit that accepted the plan, which is the artifact version the pull request is bound to.

  3. Add the workflow. It re-runs when the description is edited, and it executes the script from the base branch, so editing only scripts/check-plan-gate.sh in a pull request does not change the verdict on that pull request. A pull request that also edits the workflow can, which the paragraph after the YAML covers.

    .github/workflows/plan-gate.yml
    name: plan-gate
    on:
    pull_request:
    types: [opened, edited, synchronize, reopened]
    permissions:
    contents: read
    jobs:
    plan-gate:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v7
    with:
    fetch-depth: 0
    persist-credentials: false
    - name: Require accepted intent, spec and plan for protected paths
    run: |
    git show "origin/${GITHUB_BASE_REF}:scripts/check-plan-gate.sh" > "$RUNNER_TEMP/check-plan-gate.sh"
    bash "$RUNNER_TEMP/check-plan-gate.sh"
    env:
    PR_BODY: ${{ github.event.pull_request.body }}

    The description goes through env, never interpolated into the run line, so text in a pull request description cannot inject shell commands. The script is written to a file before it runs, so a missing script fails the step: piped into bash, a failed git show would hand it empty input and pass, because GitHub runs an unspecified run shell without pipefail.

    The workflow file itself is not protected the same way. On a pull_request event, GitHub runs the workflow file from the pull request’s own merge commit, so a pull request can edit plan-gate.yml to skip the check and still report green. The control for that is the /.github/ line in CODEOWNERS from step 1, together with Require review from Code Owners: no edit to the workflow merges without a platform owner’s approval. Without that review rule, this gate is advisory.

  4. Make the check required. Expect plan-gate to fail on the setup pull request itself, because the script is not on main yet. That is why the check is not yet required. It goes green from the next pull request onwards. After the setup pull request merges, add plan-gate as a required status check on main. Merge rights stay with people; agents open pull requests but never merge them.

  5. Split every protected change into two pull requests. The plan pull request adds changes/SLUG/ and nothing else, and its code owner approves it. The implementation pull request starts after that merge, carries Change-Dir: changes/SLUG, and records deviations in its description.

  6. Run the self-test in the next section before you announce the policy.

The artifact files and the gate are identical for every tool. What differs is how you keep the agent read-only while it plans, and how the plan reaches changes/SLUG/.

Start a planning session that cannot edit files:

Terminal window
claude --permission-mode plan

Inside an existing session, use /plan or cycle modes with Shift+Tab. Paste the planning prompt below, iterate until the plan is complete, then approve it and ask Claude Code to write only the three files under changes/SLUG/ and open the plan pull request. To make planning the default in a repository where most work is sensitive, set "permissions": { "defaultMode": "plan" } in the shared .claude/settings.json. Project settings accept plan; they ignore auto and bypassPermissions.

You prove the gate with tests of the gate, not by reading the agent’s code. Run these four checks once at rollout and again after any change to the scope or the workflow:

TestHow to run itExpected result
Protected change without a planOpen a draft pull request that edits one file under src/auth/ and has no Change-Dir: lineplan-gate fails with the “Add a line” error
Plan in the same pull requestAdd changes/test/ with all three files to that pull request and the Change-Dir: lineFails: the artifacts are not on main yet
Accepted planRemove changes/test/ from the draft, merge those three files in a separate code-owner-approved plan pull request, then rebase the draftPasses and prints the accepting commit
Plan edited during implementationChange one line of changes/test/plan.md in the draftFails with “Amend them in a separate plan PR”

Then run a fresh-session test in each tool your organization supports: open a new session with no private context and ask “What must happen before you edit db/migrations/?”. A correct answer cites docs/policies/plan-gate.md. If it does not, reference the policy from the shared rules that the shared agent rules page describes.

For ongoing evidence, count from the CI logs each month: protected-path pull requests, how many passed the gate on the first run, and how many merged through a bypass. A rising bypass count is the early signal that the scope is too broad or plan review is too slow. The acceptance record itself is the merged plan pull request, with the code owner’s approval stored by GitHub.

The plan pull request queue becomes the bottleneck. Code owners who receive long plans stop reading them. Recovery: cap plan.md at one screen plus the file list, run the “attack the plan” prompt first and attach its table, and set a response-time expectation for plan reviews in the policy.

The scope is too broad, so everything needs a plan. Engineers then write empty plans to get through. Recovery: remove any path whose last ten changes caused no incident and needed no rollback, and move it to the ordinary evidence-bundle route. Expect .github/ to cause friction on day one, because every routine workflow edit, Dependabot or Renovate action bump, and CODEOWNERS edit then needs a Change-Dir: narrow it in PROTECTED to .github/workflows/plan-gate.yml and .github/CODEOWNERS, or route bot-authored dependency bumps through a small standing changes/ci-maintenance/ plan.

Plans are approved but the diff drifts. The gate proves a plan existed, not that the code follows it, and any earlier accepted changes/SLUG/ satisfies it, so a new risky pull request can cite an old, unrelated plan. A Change-Dir serves one implementation pull request: the diff-versus-plan prompt and the reviewer confirm that every protected path the pull request touches appears in the file list of plan.md. Recovery: require the “Plan deviations” section from the third prompt in every implementation pull request, and have a review agent flag files that the plan does not name.

Someone weakens the gate. An edit to PROTECTED or to the workflow is itself a protected change. The base-branch copy of the script covers only the script: the workflow file runs from the pull request itself, so it can be edited to pass. Recovery: keep .github/ and the script under platform code owners with Require review from Code Owners turned on, which is the control that actually blocks such an edit, and keep executing the script from the base branch, as the workflow above does.

An emergency cannot wait for a plan. Recovery: use the audited ruleset bypass, name it in the incident record, and merge the retroactive plan pull request within the two days the policy sets. Review every bypass monthly.

A monorepo path moves. A renamed directory silently falls out of PROTECTED. Recovery: add a unit test that asserts each protected directory in the script still exists in the repository.

The plan gate is one control among several. Place it with the others, then connect it to what reviewers read at merge time.