Skip to content

Plan mode policy — hook + managed policy enforces planning before destructive changes

Question 17 (Org enablement): Do you have a “Plan mode required” policy for sensitive changes (DB migrations, infra, security)?

Max-score answer: Hard-enforced policy — hook + managed policy blocking change without a plan.

Why this matters in 2026 (memory can’t enforce; only the harness can)

Section titled “Why this matters in 2026 (memory can’t enforce; only the harness can)”

Plan mode is the cheapest gate against destructive changes that exists in any agentic coding tool. Press Shift+Tab twice, and Claude Code drafts a full written plan before touching a file, then waits for your approval. The agent cannot run Bash, Edit, Write, or any write-side MCP tool until you say yes. For a destructive change — a schema migration, a Terraform apply, a permissions table rewrite — that two-minute pause is the difference between “we caught it in review” and “we paged the on-call at 2am because production is gone”.

The problem is that Plan mode is opt-in. In 2025 most teams treated it as a personal preference: “use Plan mode if you feel like it”. Predictably, the engineers who needed it most — overconfident, in a hurry, working in a context they didn’t fully understand — were the ones who skipped it. By 2026 the post-mortems caught up. Cursor and Anthropic both shipped managed-policy tiers that finally give CTOs a way to enforce “Plan mode required” the only way it can actually work: at the tooling layer, before the agent has the chance to forget.

Here is the brutal truth that Q17 is built around. Memory cannot enforce “every time X”. A line in CLAUDE.md that says “always use Plan mode for migrations” is a polite suggestion. The agent will follow it most of the time, then drop it the one time it matters — usually under time pressure, on the change that breaks production. Conventions in the team wiki are even weaker. The only thing that can guarantee a behavior happens every time is the harness itself: a hook that the agent cannot disable, configured by a managed policy the user cannot override, blocking the tool call before it lands.

CTOs who max out Q17 have stopped relying on intent and started relying on enforcement. They’ve identified the file paths where destructive changes live — migrations/, terraform/, infra/, src/lib/auth/, anything matching *.sql — and they’ve built a PreToolUse hook that refuses to let Edit or Write touch those paths unless the session has a Plan-mode-approved plan attached. The hook ships through managed policy. The managed policy ships through MDM. New engineers get the gate on day one and find out about it the first time they try to bypass it.

The leverage is enormous, because the failure modes Plan mode catches are the ones that wake you up at 2am. A dropped column. A truncated index. A Terraform destroy on the wrong workspace. A force-pushed auth schema. These are not the bugs that get caught in code review — they’re the bugs that are already in production by the time review starts. A managed-policy-backed hook moves the gate from “after the damage” to “before the agent can do it”.

A max-score Q17 answer has four pieces, all working together. Miss any one and you’re in the next tier down.

1 · Sensitive-path inventory (committed to repo)
└── .claude/sensitive-paths.json (or equivalent)
- migrations/**, drizzle/**, *.sql
- terraform/**, infra/**, k8s/**, *.tf
- src/lib/auth/**, src/middleware/**
- .github/workflows/**, wrangler.toml
- any secrets-handling code paths
2 · PreToolUse hook that gates Edit/Write/Bash
└── .claude/hooks/require-plan-mode.sh
- reads sensitive-paths.json
- checks whether session has a Plan-mode-approved plan
- if path matches and no plan: exit 2 (block)
- writes every block + override to an audit log
3 · Managed policy that ships the hook org-wide
└── managed-settings.json
- hooks.PreToolUse references the script path
- the script is delivered via dotfiles or MDM
- users cannot disable via disableAllHooks
4 · Explicit escape hatch + audit log
└── --i-have-a-plan flag or signed plan file
- emergency path for legitimate hotfixes
- every use writes to ~/.claude/plan-overrides.log
- audit log shipped to central log store weekly

The key word is enforced. A document that says “please use Plan mode” is the “Reactive” tier. A CLAUDE.md reminder is “Coordinated”. A hook in the project’s .claude/settings.json that any developer can disable locally is “Optimized” at best. Only a hook that is delivered by managed policy, references a committed sensitive-paths list, blocks via exit 2, and logs every block and override to a central audit trail puts you at “Strategic Leader”.

Managed policy in Claude Code (admin-deployed settings.json)

Section titled “Managed policy in Claude Code (admin-deployed settings.json)”

The managed-settings tier is what makes “hard-enforced” possible at all. Anthropic shipped managed settings as part of the Team/Enterprise plans: a JSON file on every developer’s machine, resolved before user settings, project settings, and CLI flags. Anything it sets cannot be overridden by a developer flag or a project file. The file paths are OS-specific:

macOS: /Library/Application Support/ClaudeCode/managed-settings.json
Linux/WSL: /etc/claude-code/managed-settings.json
Windows: C:\ProgramData\ClaudeCode\managed-settings.json

For Plan-mode enforcement, two managed-policy levers matter most. First, hooks.PreToolUse declares a script that fires before every tool call — the agent cannot proceed until the script exits cleanly. Second, disableAllHooks: true set at user, project, or local level cannot disable managed-policy hooks — only disableAllHooks set at the managed tier itself can. That asymmetry is the entire point: an organization can ship a Plan-mode gate that a developer in a hurry physically cannot turn off.

Exit codes matter here. Developers default to exit 1 because that’s the Unix convention for “command failed”. Claude Code treats exit 1 as a non-blocking error and proceeds with the tool call anyway. For policy enforcement you must exit 2. Get this one detail wrong and your hook becomes a polite log line instead of an actual block — the kind of failure mode that only shows up the day someone destroys production and you discover the gate was off.

Hook that blocks Edit/Write on sensitive paths without a plan

Section titled “Hook that blocks Edit/Write on sensitive paths without a plan”

The hook itself is short. It runs as a PreToolUse event, receives the pending tool call on stdin, decides whether the path is sensitive, and decides whether the session has a valid plan. If sensitive and no plan, it exits 2 with a clear message explaining how to retry in Plan mode.

.claude/hooks/require-plan-mode.sh
#!/usr/bin/env bash
# PreToolUse hook: block Edit/Write/Bash on sensitive paths without Plan mode.
# Exit 2 = block (Claude Code policy convention). Exit 0 = allow.
set -euo pipefail
INPUT="$(cat)" # JSON: { tool, params, session_id, ... }
TOOL=$(jq -r '.tool' <<<"$INPUT")
# Only gate write-side tools. Reads pass through.
case "$TOOL" in
Edit|Write|MultiEdit|NotebookEdit) ;;
Bash)
CMD=$(jq -r '.params.command // ""' <<<"$INPUT")
# Allow read-only bash. Gate writes, network calls, anything destructive.
case "$CMD" in
ls*|cat*|grep*|rg*|head*|tail*|git\ status*|git\ diff*|git\ log*) exit 0 ;;
esac
;;
*) exit 0 ;;
esac
# Extract target path(s) for file-write tools.
TARGET=$(jq -r '.params.file_path // .params.notebook_path // empty' <<<"$INPUT")
# Sensitive-path patterns, committed at .claude/sensitive-paths.json.
PATTERNS=$(jq -r '.patterns[]' "$CLAUDE_PROJECT_DIR/.claude/sensitive-paths.json")
is_sensitive=false
for p in $PATTERNS; do
case "$TARGET" in
$p) is_sensitive=true; break ;;
esac
done
# Also gate bash commands that match destructive patterns.
if [ "$TOOL" = "Bash" ]; then
CMD=$(jq -r '.params.command' <<<"$INPUT")
case "$CMD" in
*"drizzle-kit push"*|*"prisma migrate deploy"*|*"terraform apply"*\
|*"kubectl apply"*|*"wrangler d1 execute"*|*"rm -rf"*) is_sensitive=true ;;
esac
fi
[ "$is_sensitive" = false ] && exit 0
# Sensitive path. Is the session in Plan mode with an approved plan?
PLAN_FILE="${CLAUDE_PROJECT_DIR}/.claude/plans/${CLAUDE_SESSION_ID}.md"
if [ -f "$PLAN_FILE" ] && [ "$(stat -f%z "$PLAN_FILE" 2>/dev/null || stat -c%s "$PLAN_FILE")" -gt 200 ]; then
# Plan exists and is non-trivial. Allow, but log.
echo "$(date -u +%FT%TZ) ALLOW $TOOL $TARGET session=$CLAUDE_SESSION_ID" \
>> "$HOME/.claude/plan-audit.log"
exit 0
fi
# Block, with a clear remediation message on stderr.
cat >&2 <<MSG
Plan mode required for sensitive change.
Tool: $TOOL
Target: $TARGET
This path is gated by the org-wide Plan-mode policy. To proceed:
1. Switch to Plan mode (Shift+Tab twice).
2. Draft the change; the plan will be saved to .claude/plans/.
3. Approve the plan to exit Plan mode.
4. Retry the tool call — it will be allowed and audited.
Override path (incident hotfix only): touch .claude/plans/\${CLAUDE_SESSION_ID}.override
Every override is logged to ~/.claude/plan-audit.log and shipped to the audit store weekly.
MSG
echo "$(date -u +%FT%TZ) BLOCK $TOOL $TARGET session=$CLAUDE_SESSION_ID" \
>> "$HOME/.claude/plan-audit.log"
exit 2

The script is intentionally short. Long hooks become unmaintainable; nobody reviews them, drift sets in, the gate quietly weakens. Keep it under 80 lines, commit it to a central dotfiles-style repo, sign the commits, and let MDM push it to every developer’s ~/.claude/hooks/ on first login.

Cursor’s equivalent enforcement tier is Team Rules: org-admin-defined rules that propagate to every workspace in the Cursor team, sitting above user rules and project rules in the precedence chain. Cursor doesn’t expose a PreToolUse hook with the same shape as Claude Code, but Team Rules can declare “Plan mode required for migrations/** and terraform/**” as a hard rule that the agent treats as non-negotiable.

For Cursor-first orgs, the practical pattern is: ship the rule via Team Rules so the agent refuses to act without a plan, and layer a server-side guardrail (a pre-commit hook, a server-side branch protection, an OPA policy in CI) that catches anything that slips through. Cursor’s enforcement is softer than Claude Code’s managed-policy + PreToolUse combination, so the CI layer matters more.

If your org runs both tools, mirror the gate on both sides. Half-coverage is worse than no coverage because it creates a false sense of safety.

What “sensitive” means in your org (DB, infra, security, auth)

Section titled “What “sensitive” means in your org (DB, infra, security, auth)”

The sensitive-path list is the most important artifact in this entire setup, because the gate is only as good as the patterns it watches. Get it too narrow and the agent walks around it; get it too broad and developers learn to bypass it. A realistic baseline for most production codebases:

  • Database: migrations/**, drizzle/**, prisma/migrations/**, *.sql, scripts/schema*.sql, anything that mutates schema.
  • Infrastructure: terraform/**, infra/**, k8s/**, *.tf, wrangler.toml, Dockerfile, deployment manifests.
  • Security: src/lib/auth/**, src/middleware/**, anything handling tokens, sessions, OAuth callbacks, RBAC matrices.
  • Secrets-adjacent: .env* (templates only — real secrets never in repo), *.tfvars, wrangler secret invocations.
  • CI/CD: .github/workflows/**, .gitlab-ci.yml, anything that runs in deploy pipelines.
  • Destructive bash patterns: drizzle-kit push, prisma migrate deploy, terraform apply, kubectl apply, wrangler d1 execute, rm -rf, git push --force.

Commit the list to the repo as .claude/sensitive-paths.json. Review it every quarter as part of the rules audit. When an incident happens, the post-mortem should ask “would the gate have caught this?” and update the list if the answer is no.

Step-by-step: rolling out hard-enforced Plan mode

Section titled “Step-by-step: rolling out hard-enforced Plan mode”
  1. Inventory your sensitive paths before writing a single line of hook code.

    Walk every active production repo. List every directory and file pattern that — if an agent silently rewrote it — would cause an incident. Database migrations, infrastructure-as-code, auth middleware, CI pipelines, secret handling. Get the list out of one engineer’s head and into .claude/sensitive-paths.json, committed and reviewed.

  2. Write the hook against the smallest possible surface first.

    Start with migrations/** only. Get the PreToolUse script working end-to-end: it reads the input, identifies the tool, checks the path, exits 2 with a clear message when the path matches and no plan is attached. Run it locally for a week before you ship it. Confirm exit 2 actually blocks — many teams discover their hook is silently a no-op because they used exit 1.

  3. Decide what “has a plan” means in your environment.

    Two viable patterns. First, Plan-file-on-disk: when the user approves a plan in Claude Code Plan mode, write the markdown to .claude/plans/<session_id>.md and let the hook check for it. Second, trace marker: rely on a session-level environment variable or a flag the agent passes. The plan-file pattern is simpler to audit and works across tools — pick that one unless you have a reason not to.

  4. Add an escape hatch for genuine emergencies.

    The hook will be wrong sometimes. A production hotfix at 3am cannot wait for a full plan-mode drafting cycle. Provide a documented override: touch .claude/plans/<session_id>.override or a --i-have-a-plan CLI flag. Every override writes a row to the audit log with timestamp, user, repo, path, and tool. Overrides are not banned — they are logged.

  5. Ship the hook through managed policy, not project settings.

    A hook in .claude/settings.json is bypassable; any developer can delete the file or set disableAllHooks: true locally. The same hook referenced from managed-settings.json survives every local override. Put the script in a central dotfiles-style repo, push it to ~/.claude/hooks/require-plan-mode.sh via MDM or a bootstrap script with hash verification, and reference it from the managed file.

  6. Mirror the gate in Cursor Team Rules (if applicable).

    If part of your org uses Cursor, ship the equivalent rule via Team Rules so a Cursor-using developer doesn’t have an unprotected lane. The wording: “For any change in migrations/, terraform/, or src/lib/auth/, you must produce a plan first. Do not edit files in these directories without explicit user confirmation of the plan.”

  7. Add a CI-side backstop for anything that slips through.

    Even with the hook, assume some changes will escape — an agent on a non-managed machine, a developer running a fork, a Cursor user without Team Rules. Branch protection plus a CI job that checks “PRs touching migrations/** require the plan-approved label, applied by a separate human reviewer” catches the rest. Defense in depth: the hook is the cheap gate, CI is the expensive one.

  8. Wire the audit log to your central log store.

    Every block, every override, every “allow” on a sensitive path writes a row to ~/.claude/plan-audit.log. Ship that file to your central log store nightly (S3, Datadog, Loki, whatever you use). When you need to answer “did the agent change this migration without a plan?” the answer should be one query away, not “let me ssh into someone’s laptop”.

  9. Run a tabletop exercise in week two.

    Pick the scariest plausible failure mode — say, “an agent in a non-Plan-mode session rewrites auth.ts and pushes to a branch”. Try to do it. If the hook blocks you, good. If anything in the chain lets you through, fix that thing before you move on. Tabletop exercises catch the failures that real incidents would discover at the worst possible time.

  10. Re-run the sensitive-path audit every quarter.

    Codebases drift. New directories show up that handle sensitive concerns. Old directories get deprecated. The audit is a 30-minute meeting once a quarter: open the repo, walk the directory tree, ask “would I be unhappy if an agent silently rewrote this without a plan?” Update sensitive-paths.json accordingly, commit, ship.

Over-broad gate that trains developers to bypass it. If the hook fires on every Edit to a *.md file or every read-only Bash command, developers will set disableAllHooks: true in their personal config — and the gate is gone. Scope the patterns tightly. The right rule is “if the agent silently changed this, I’d be paged” — not “this file is somewhat important”.

No escape hatch. A hook with no override path is a gate that gets ripped out the first time it gets in the way of a production hotfix. The right answer is “overrides are allowed and logged”. Make the override harder than the happy path — a manual file touch, a CLI flag, a [override] token in the commit message — but never impossible.

exit 1 instead of exit 2. The single most common mistake in Claude Code hook authoring. exit 1 produces a log line and the tool call proceeds anyway. exit 2 actually blocks. Verify with a one-line test the day you ship the hook, then again every quarter, because the only thing worse than a missing gate is one that you think is in place.

Hook in project .claude/settings.json instead of managed policy. A gate that any developer can disable locally with one line of JSON is theatre, not enforcement. The hook must come from the managed-settings tier — that’s the only tier where disableAllHooks cannot reach it.

No audit log. If the hook blocks an action and nobody knows, the gate is invisible to leadership. If the hook lets an action through (legitimately or via override) and nobody knows, you can’t trace incidents back to “was the gate engaged?” Every block, every allow on a sensitive path, every override writes a row. Ship the log centrally.

Plan mode required, but no Plan-mode training. Engineers who’ve never used Plan mode treat the gate as friction and resent it. Two hours of training in the first week of rollout — “here’s how to draft a plan, here’s how to iterate on it, here’s how to approve” — turns the gate from “annoying” to “the way we work”.

Hook script lives in a single engineer’s dotfiles. If the canonical hook script is in one person’s repo and they leave, the gate becomes unmaintainable. Move it to a team-owned ai-platform-internal or dotfiles repo, with signed commits and a real PR review process.

Treating the hook as a substitute for code review. The gate prevents silent destructive changes. It does not prevent intentional bad changes by an engineer who’s wrong about what they’re doing. Plan mode + the hook is one layer; PR review by a human who understands the system is another. Both are required.

Forgetting Cursor users. A managed-policy hook in Claude Code does nothing for developers on Cursor. If your org runs both tools and you only gate one, you’ve created an unprotected lane that the wrong incident will find. Mirror the gate or pick one tool.

  • A committed .claude/sensitive-paths.json exists in every production repo, covering migrations, infra-as-code, auth, CI/CD, and destructive bash patterns.
  • A PreToolUse hook script exists, exits 2 on block, and has been tested end-to-end with a real Edit call against a sensitive path.
  • The hook is referenced from managed-settings.json (or managed-settings.d/*.json), not just project .claude/settings.json.
  • The managed-settings file is installed on every developer’s machine via MDM or a bootstrap-with-hash-verification script.
  • disableAllHooks: true in a developer’s personal config has been tested and does not disable the Plan-mode hook.
  • Cursor users in the org have a Team Rule that mirrors the gate (or there are no Cursor users).
  • An escape hatch exists, is documented, and writes every use to a central audit log.
  • The audit log is shipped to your central log store at least daily.
  • A tabletop exercise within the last 90 days confirmed the gate actually blocks the scariest plausible bypass.
  • The sensitive-paths list is reviewed every quarter and updated against the latest codebase.
  • New engineers see the gate fire during their first-week onboarding session, so they learn it as “the way we work” rather than friction.
  • A CI-side backstop (branch protection + label requirement on PRs touching sensitive paths) catches anything that bypassed the hook.