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.
What this plan policy gives you
Section titled “What this plan policy gives you”- 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.
Why is Plan mode not a planning policy?
Section titled “Why is Plan mode not a planning policy?”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.
| Tool | Plan-first feature (checked 2026-09-26 unless noted) | What it enforces | What it cannot prove |
|---|---|---|---|
| Claude Code 2.1.283 | plan permission mode: reads and research, edits blocked until you approve the plan. Enter with /plan, Shift+Tab, or claude --permission-mode plan | No file edits during the planning session | That 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 run | No writes while read-only | codex 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 |
| Cursor | Plan Mode “creates detailed implementation plans before writing any code” (Cursor docs, checked 2026-08-28) | Planning before code in that conversation | Acceptance, 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.
Which changes need an accepted plan?
Section titled “Which changes need an accepted plan?”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.
| Trigger | Example paths | Why a plan is mandatory | Named approver |
|---|---|---|---|
| Schema or data migration | db/migrations/ | Hard to reverse; order of deploy and backfill matters | Data platform code owner |
| Authentication, authorization, sessions | src/auth/ | A wrong default locks users out or lets the wrong ones in | Security code owner |
| Money: billing, pricing, entitlements | src/billing/ | Silent errors cost revenue or refunds | Payments code owner |
| Infrastructure, CI, and deploy config | infra/, .github/ | Changes what “green” and “deployed” mean for every other change | Platform code owner |
| Public API contracts | api/openapi.yaml | External consumers break without a failing test here | API owner |
| Sensitive data flows | files that read or export personal data | Regulatory exposure | Privacy 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.
Adopt the policy file
Section titled “Adopt the policy file”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.
# Plan gate policy
Owner: VP Engineering. Reviewed: quarterly. Version: 1.
## ScopeA 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 artifactsEach protected change has a directory changes/SLUG/ with intent.md, spec.mdand plan.md (templates: docs/templates/). The artifacts are accepted when a pullrequest that adds them is approved by the code owner of the affected area andmerged to main. Main is the only record of acceptance.
## ImplementationThe implementation pull request names its directory with a line"Change-Dir: changes/SLUG" in its description. It must not edit the acceptedartifacts. A material deviation (new file outside plan.md, changed data shape,changed rollout or rollback) stops the work: amend the plan in its own pullrequest first. A Change-Dir serves one implementation pull request, and everyprotected path that pull request touches must appear in the file list of plan.md.
## EnforcementThe plan-gate CI check is required on main. Code-owner review is required forchanges/, the protected paths, and the gate itself. Write access means writeaccess to main: agents may draft on a branch, but nothing that touches aprotected path reaches main without an accepted plan. Agents never hold mergerights or production credentials.
## ExceptionsEmergency fixes use the audited ruleset bypass, are named in the incidentrecord, and get a retroactive plan pull request within two business days.The platform team reports every bypass in the monthly engineering review.How do you set up the plan gate?
Section titled “How do you set up the plan gate?”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.
-
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 formain.# .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/platformReplace the
@acme/...teams with your own. If one architecture group is too coarse for/changes/, give change directories a domain prefix, for examplechanges/billing-SLUG/, and add/changes/billing-*/ @acme/paymentsbelow 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. -
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 artifactsset -euo pipefailBASE="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" ]; thenecho "plan-gate: no protected paths changed"exit 0fiprintf '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" ]; thenecho "::error::Add a line 'Change-Dir: changes/SLUG' to the PR description."exit 1fifor f in intent.md spec.md plan.md; doif ! git cat-file -e "$BASE:$dir/$f" 2>/dev/null; thenecho "::error::$dir/$f is not on $BASE. Merge the plan PR, approved by a code owner, first."exit 1fidoneif grep -q "^$dir/" <<<"$changed"; thenecho "::error::This PR edits the accepted artifacts in $dir. Amend them in a separate plan PR."exit 1fiecho "plan-gate: $dir accepted at $(git log -1 --format=%h "$BASE" -- "$dir")"Keep
PROTECTEDidentical 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. -
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.shin 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-gateon:pull_request:types: [opened, edited, synchronize, reopened]permissions:contents: readjobs:plan-gate:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v7with:fetch-depth: 0persist-credentials: false- name: Require accepted intent, spec and plan for protected pathsrun: |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 therunline, 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 intobash, a failedgit showwould hand it empty input and pass, because GitHub runs an unspecifiedrunshell withoutpipefail.The workflow file itself is not protected the same way. On a
pull_requestevent, GitHub runs the workflow file from the pull request’s own merge commit, so a pull request can editplan-gate.ymlto skip the check and still report green. The control for that is the/.github/line inCODEOWNERSfrom 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. -
Make the check required. Expect
plan-gateto fail on the setup pull request itself, because the script is not onmainyet. That is why the check is not yet required. It goes green from the next pull request onwards. After the setup pull request merges, addplan-gateas a required status check onmain. Merge rights stay with people; agents open pull requests but never merge them. -
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, carriesChange-Dir: changes/SLUG, and records deviations in its description. -
Run the self-test in the next section before you announce the policy.
Produce the artifacts in each tool
Section titled “Produce the artifacts in each tool”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:
claude --permission-mode planInside 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.
In the interactive CLI, run /plan and paste the planning prompt. For a scripted, read-only draft, write the final message to a file and commit it yourself:
codex exec --sandbox read-only -o /tmp/plan-draft.md \ "Read changes/2026-09-sso/intent.md and spec.md. Draft plan.md following docs/templates/plan.md. Do not modify files."This uses the legacy --sandbox read-only, which codex exec still accepts in 0.157.1, and it blocks writes for the whole run, which is a stronger guarantee than a mode the session can leave. OpenAI now prefers permission profiles (beta): codex exec -c default_permissions=":read-only" .... Use one or the other, because OpenAI says the two systems do not compose. Copy the draft into changes/2026-09-sso/plan.md and open the plan pull request.
Switch the agent to Plan Mode before you paste the planning prompt. Review and edit the plan in the conversation, then switch back to Agent and ask it to write only the three files under changes/SLUG/ and nothing else. The plan pull request, not the conversation, is the record of acceptance.
Copy-paste prompts for the plan gate
Section titled “Copy-paste prompts for the plan gate”How do you know the plan gate works?
Section titled “How do you know the plan gate works?”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:
| Test | How to run it | Expected result |
|---|---|---|
| Protected change without a plan | Open a draft pull request that edits one file under src/auth/ and has no Change-Dir: line | plan-gate fails with the “Add a line” error |
| Plan in the same pull request | Add changes/test/ with all three files to that pull request and the Change-Dir: line | Fails: the artifacts are not on main yet |
| Accepted plan | Remove changes/test/ from the draft, merge those three files in a separate code-owner-approved plan pull request, then rebase the draft | Passes and prints the accepting commit |
| Plan edited during implementation | Change one line of changes/test/plan.md in the draft | Fails 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.
What breaks when you enforce a plan gate?
Section titled “What breaks when you enforce a plan gate?”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.
Where to go next with plan governance
Section titled “Where to go next with plan governance”The plan gate is one control among several. Place it with the others, then connect it to what reviewers read at merge time.