Skip to content

All posts

AI

How to pin an AI agent to its approved plan

Ritesh Patel

AI: How to pin an AI agent to its approved plan

To pin an agent to its approved plan, allowlist the exact tool calls the plan needs, deny what it excludes, and check risky arguments in a PreToolUse hook. These controls limit what the agent can run. They do not detect when its reasoning has drifted from the goal.

Checked against the Claude Code permissions, hooks and permission-modes docs as fetched on 2026-11-12, and tested on Claude Code 2.1.321. In that test, in dontAsk mode, an allowed command ran, a git push matching a deny rule was blocked even though the hook allowed it, and an allowed command missing from the plan file was blocked by the hook. The hook script had no executable bit and sat under a path with a space. On a folder whose trust dialog had not been accepted, Claude Code ignored the project allow rules, so every command that needed one was denied.

What pinning can and cannot do

An approved plan is a statement of intent. An agent acts through tool calls, so the enforceable version of the plan is the set of calls it may make: which tools, which commands and which arguments. Allowlisting turns that set into rules the client applies before a call runs.

This bounds what goal drift can do. It does not tell you that drift happened. If the agent starts asking for calls outside the plan, the rules block them, but the only sign is the denial itself. Keep an audit trail of attempted calls next to the rules, as described in our post on auditing what an AI coding agent did.

How Claude Code decides a tool call

Three parts of Claude Code decide whether a plan holds: the permission mode you run in, the allow and deny rules, and any PreToolUse hook. The rule order (deny, then ask, then allow) and the precedence between rules and hooks are covered in our tool-level authorization post. This post covers the parts that matter for a plan.

Mode What runs without asking Plan-specific effect
plan Reads, plus classifier-approved commands when auto mode is available Edits stay blocked until you approve a plan, except in interactive terminal sessions with bypass permissions available
dontAsk File reads in your working directories and pre-approved tools Anything that would prompt is denied, so the allowlist becomes the whole permission surface

Turn the plan into exact allow rules

Start by writing the plan as a short list of commands and tools, one line each. Include only what the task needs. Then translate each line into a rule in the project settings file, .claude/settings.json, which applies to everyone who works in that repository. Use ~/.claude/settings.json only for rules that should apply to all your projects.

  1. Prefer exact-match Bash rules. Bash(npm test) matches that exact command. A rule with * covers more, so Bash(npm run *) matches every npm script.
  2. For MCP tools, allow the full name, mcp__<server>__<tool>. The name mcp__puppeteer__puppeteer_navigate is the form the docs use. Use mcp__<server>__* only when every tool from that server belongs in the plan.
  3. For a runner such as devbox run, write the inner command into the rule. A rule for devbox run * matches whatever follows run, including devbox run rm -rf ..
  4. Leave out anything the plan does not name. A missing allow rule is not the same as a block, as the next section explains.
{
  "permissions": {
    "allow": [
      "Bash(npm test)",
      "Bash(npm run lint)",
      "mcp__puppeteer__puppeteer_navigate"
    ],
    "deny": [
      "Bash(git push *)"
    ]
  }
}

Deny what the plan excludes, and run the task without prompts

An allow list alone is not a pin. In default mode, a call with no matching rule still asks you, and you may approve it under pressure. Add deny rules for anything the plan rules out, then start the execution phase in a mode that refuses whatever is left.

  1. Use a bare tool name in a deny rule for a tool the plan never needs. "WebFetch" removes that tool from Claude’s context, so the model never sees it.
  2. Use a scoped deny rule, such as Bash(git push *), when the tool is needed for some calls only. The tool stays available, and matching calls are blocked.
  3. Start the planning phase with claude --permission-mode plan, then approve the plan.
  4. Start the execution phase with claude --permission-mode dontAsk. It denies anything that would prompt.

Check arguments in a PreToolUse hook

An allow rule matches a tool and a command pattern. It cannot check every argument against the plan. A PreToolUse hook can. It runs before each matching call and receives the call as JSON on stdin, with tool_name and tool_input fields.

  1. Register a PreToolUse hook in settings. Use a matcher for the tool. For every tool from an MCP server, write mcp__<server>__.*. The .* is required, because mcp__<server> on its own matches no tool.
  2. Write .claude/hooks/check-plan.sh yourself. It reads the PreToolUse JSON from stdin, compares tool_input with your approved plan file, and exits 2 to block a call that is not in the plan. Keep the plan file in the repository so it is reviewed like any other config.
  3. When a call is outside the plan, you can also print the deny JSON below to stdout instead of exiting 2.
  4. Do not rely on the hook alone. A timed-out command hook does not block the call by itself on PreToolUse, and the if filter is best-effort. Use permission rules for hard limits.
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"${CLAUDE_PROJECT_DIR}/.claude/hooks/check-plan.sh\""
          }
        ]
      }
    ]
  }
}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Call is not in the approved plan"
  }
}

Gotchas that break a plan pin

Rule-level gotchas (chained commands, MCP parentheses, deny precedence) are covered in the tool-level authorization post. Two matter most for a plan.

  • In dontAsk mode, MCP tools marked as requiring user interaction and connector tools your organization set to ask are denied even when an allow rule lists them. Plan the allowlist around that.
  • A project’s allow rules apply only after you accept the workspace trust dialog for that folder, so a plan file committed to a repo does nothing on an untrusted checkout.

Checklist before you trust the pin

Work through this list before you rely on the pin for a real task. Run each allow and deny rule once in a throwaway copy of the repository, on the Claude Code version your team uses, and record the version with the settings file.

  1. Each line of the plan maps to an exact allow rule or to a hook check.
  2. Every excluded tool or command has a deny rule, not just a missing allow entry.
  3. The execution phase runs with --permission-mode dontAsk.
  4. The hook command quotes ${CLAUDE_PROJECT_DIR} and runs the script through bash, so the script does not need an executable bit. It returns the deny JSON for off-plan calls.
  5. Deny rules that must hold are in managed settings, where no lower scope can override them, and you have checked your mods.

Where this fits

A session-level pin is one layer. For the broader model, read how tool-level authorization works for AI agents and admission control versus runtime enforcement for AI agents. Our guide to governing AI agents covers the wider program, and the AIControls platform page describes the product.

Sources checked on 2026-11-12: Claude Code permissions, permission modes and hooks.

Frequently asked questions

Does an allowlist stop an agent from drifting off its goal?

Not by itself. An allowlist limits which tool calls can run, so it bounds the damage an off-plan agent can do. It does not detect that the agent's reasoning has changed. Pair the allowlist with an audit trail of what the agent attempted, so a person can see when it started asking for calls outside the plan and review them.

Why does my allow rule not cover a chained command?

Claude Code treats &&, ||, ;, |, |&, & and newlines as command separators. A rule such as Bash(safe-cmd *) does not approve safe-cmd && other-cmd. Every subcommand of a compound command must be allowed, either by its own allow rule or by being a built-in read-only command. Write one rule for each command you want to permit.

What happens when a PreToolUse hook allows a call that a deny rule blocks?

The deny rule still wins. Claude Code evaluates deny and ask rules regardless of what a PreToolUse hook returns. A hook can add a block, but returning allow does not override a matching deny or ask rule. A hook that exits with code 2 blocks the call even when an allow rule would permit it.

Can I allow an MCP tool only for certain argument values?

Not with an allow rule. MCP allow rules take no parenthesized specifier, and Claude Code skips an mcp__ rule that has parentheses when it loads a settings file. Parameter matching for MCP tools is passed as a deny rule with the --disallowedTools flag. For a per-call check on arguments, use a PreToolUse hook that reads the tool input and denies calls outside the plan.

Does plan mode lock the agent to the plan I approved?

No. Plan mode blocks edits until you approve a plan, and approving it switches the session to the permission mode you choose. Plan mode does not turn the plan into rules. The allowlist, deny rules and hook still have to be written in settings, and they keep applying after the plan is approved.

Ritesh Patel, Founder & VP Product, Nirmata

Ritesh Patel is a founder of Nirmata and leads its products. He writes about governing AI coding agents, MCP security and Kubernetes policy as code.

LinkedIn

Govern AI agents with AIControlsControl what AI agents do, and prove it.
ShareXLinkedIn

Stay up-to-date with the latest in AI, governance, and infrastructure

Subscribe to our free monthly newsletter to get industry news, DevOps tips, and articles right to your inbox on Kyverno, containerization, Kubernetes deployment & management and more.