# AI Agent Tool Allowlists: Deny by Default, Scope per Task, Audit Everything

One Claude Code session declares 76 tools; 64 of them never fire. A tool allowlist is the list of calls your agent may make — everything else denied. How to set one in Claude Code, Codex CLI, and MCP, and where client-side lists stop holding.

An **AI agent tool allowlist** is the explicit set of tools an agent is permitted to call — everything not on the list is denied by default. It's the single highest-leverage control you can put on an agent, because an agent's capability isn't what it *does*, it's what it *can* do: in one real Claude Code session we captured, the harness declared **76 tools** to the model, and 64 of them were never invoked. Every one of those 64 is standing capability — send email, publish a web page, schedule its own future runs — waiting on nothing but a model deciding to use it.

This page is the reference: what an allowlist is, how to configure one in Claude Code, Codex CLI, and MCP, how to scope it per task, and — honestly — where a client-side allowlist stops holding and what to do about that.

## Why deny-by-default, and where it's wrong

Deny-by-default is the right posture for the same reason it's the right posture everywhere else in security: the cost asymmetry. A denied tool the agent didn't need costs you nothing. An allowed tool it misused costs you whatever that tool could do — and for tools that send, pay, or delete, that's not a rollback.

But apply it naively and you get the failure mode every developer who's tried it knows: an agent that asks permission to read a file makes hundreds of asks per session, and by day two you're approving on reflex. So the practical rule, [argued in full here](/blog/which-claude-code-tools-to-deny-out-of-the-box), is:

- **Allow the core loop** — read, search, edit, execute-in-sandbox. That's the agent's hands.
- **Deny the outward tail** — send, publish, schedule, remote-spawn — until first legitimate need. Un-deny one tool at a time, not a family.
- **Gate the irreversible** behind a human, if you have an approval mechanism that a human actually reads.

The [Tool Surface Index](/tool-surfaces) maintains live captures of what each coding agent actually declares, grouped by blast radius, with a verdict per family. Sort your own agent's tools into those families and the allowlist writes itself.

## How to set a tool allowlist in Claude Code

Claude Code reads permission rules from `settings.json` (project-level `.claude/settings.json` or user-level `~/.claude/settings.json`):

```json
{
  "permissions": {
    "allow": [
      "Read",
      "Edit",
      "Bash(npm run test:*)",
      "Bash(git diff:*)"
    ],
    "deny": [
      "WebFetch",
      "Read(./.env)",
      "Read(./secrets/**)",
      "Bash(curl:*)"
    ]
  }
}
```

Three things worth knowing that the examples don't show:

1. **Bash rules are prefix matches on the command string**, not a semantic understanding of the command. `Bash(git diff:*)` allows anything starting `git diff`. Anthropic's own docs are upfront that Bash deny rules can be sidestepped by how a command is written — treat them as guardrails against accidents, not a security boundary. (More on this below.)
2. **`deny` beats `allow`** — a rule in both lists is denied.
3. **MCP tools use the form `mcp__servername__toolname`** — so a connected MCP server's tools can be allowlisted individually, not just as a server.

## How to set one in Codex CLI

Codex ships a deliberately tighter surface — [17 declared tools](/tool-surfaces) against Claude Code's 76 — and controls it with approval policies and sandbox modes rather than per-tool lists, in `~/.codex/config.toml`:

```toml
approval_policy = "untrusted"     # ask before any command not on the trusted list
sandbox_mode    = "workspace-write"  # writes confined to the workspace
```

The trade: less granularity, but the sandbox does structural work a string-matched list can't. What Codex doesn't give you is per-tool allow/deny on its non-shell tools — the practical route to that is governing at the MCP boundary or the model proxy, same as below.

## How to allowlist MCP tools

An MCP server is a package of tools, and connecting one is a *family-sized* grant — the calendar server you added for scheduling also reads every event. Two levels:

- **Client-side:** the `mcp__server__tool` rules above — real granularity, but they live in a config file the agent's host can edit, and they only bind that one client.
- **Server-side (gateway):** front the MCP server with a gateway that exposes only an enabled subset of tools per workspace. The agent's `tools/list` simply never includes the rest. Nothing to bypass client-side, because the denied tools don't exist from the agent's point of view. This is how ACP does it — the allowlist is enforced where the tool call lands, not where it's emitted.

## Scope per task, not per install

A standing allowlist answers "what may this agent *ever* do." The better question is "what does it need for *this* task" — a code-review agent needs read and comment; the same harness doing a release needs push and publish. Two mechanisms make per-task scoping cheap:

- **Tiers.** Split policy by context: interactive (you're watching) vs background (cron jobs, CI, headless runs). The same tool can be `ask` interactively and `deny` in background — an agent running at 3am has no one to ask.
- **Flip-on-first-need.** Start narrow. When the agent legitimately needs a denied tool, the deny message tells you which rule fired; un-denying one tool is a one-line change. That workflow beats guessing the full list upfront, every time.

The deeper version of this argument — narrow, typed tools as the precondition for any meaningful policy — is in [agent access control with scoped tools](/blog/agent-access-control-scoped-tools).

## Where client-side allowlists stop holding

Now the honest part. A client-side allowlist — settings.json, hooks, any check that runs inside the agent's own process — holds against *mistakes*, which is most of what goes wrong. It does not hold against the two things people quietly assume it does:

1. **String-matching misses semantics.** A denied `Bash(rm:*)` doesn't catch the same deletion expressed as a script the agent wrote and then executed. The effects of a shell command aren't in its prefix. We walk the documented bypass classes in [Claude Code's deny list can be bypassed](/blog/claude-code-deny-list-bypass).
2. **The client can opt out.** Any check that lives in the client's own config can be edited out by whoever has write access to that config — a settings file is not a boundary. (`--dangerously-skip-permissions` is not that opt-out: it removes prompts, and [hooks and deny rules still run](/blog/claude-code-dangerously-skip-permissions).) An allowlist the client enforces is an allowlist the client can drop.

The fix isn't a cleverer regex; it's moving enforcement to a boundary the agent can't route around: a gateway that executes the tool calls (denied tools don't exist), or a model proxy that strips denied `tool_calls` before they ever reach the harness. Client-side rules stay useful — they're fast, local, and catch the accidents — but the list that has to hold lives outside the agent's process.

## Audit is the allowlist's other half

An allowlist you can't audit decays silently. Harnesses lazy-load tools, MCP servers connect mid-session, updates add families — in our captured traffic, one session **gained 21 tools partway through the day** with no prompt and no changelog. The allowlist you wrote in January is matched against a surface you counted in January.

So the operational loop is: capture the declared surface, diff it across sessions, and log every allow/deny decision with the rule that made it. That's the part that turns a config file into a control. Doing it continuously — every declaration captured, every change diffed, every tool a click to allow, flag, or deny — is what [ACP does for coding agents](/for-coding-agents):

```bash
curl -sf https://agenticcontrolplane.com/install.sh | bash
```

But the posture stands with any tooling: deny by default, allow the loop, scope to the task, and re-check the count — because the allowlist is only as current as your last look at the surface.

<!-- FAQPage schema. Every answer mirrors sentences that are visible in the post body above — do not add Q&As whose text isn't on the page. -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is an AI agent tool allowlist?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "An AI agent tool allowlist is the explicit set of tools an agent is permitted to call — everything not on the list is denied by default. It's the single highest-leverage control you can put on an agent, because an agent's capability isn't what it does, it's what it can do: in one real Claude Code session we captured, the harness declared 76 tools to the model, and 64 of them were never invoked."
      }
    },
    {
      "@type": "Question",
      "name": "How do you set a tool allowlist in Claude Code?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Claude Code reads permission rules from settings.json (project-level .claude/settings.json or user-level ~/.claude/settings.json), with allow and deny lists per tool. Bash rules are prefix matches on the command string, not a semantic understanding of the command; deny beats allow — a rule in both lists is denied; and MCP tools use the form mcp__servername__toolname, so a connected MCP server's tools can be allowlisted individually."
      }
    },
    {
      "@type": "Question",
      "name": "How do you set a tool allowlist in Codex CLI?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Codex ships a deliberately tighter surface — 17 declared tools against Claude Code's 76 — and controls it with approval policies and sandbox modes rather than per-tool lists, in ~/.codex/config.toml (approval_policy, sandbox_mode). The trade: less granularity, but the sandbox does structural work a string-matched list can't. Codex doesn't give you per-tool allow/deny on its non-shell tools — the practical route to that is governing at the MCP boundary or the model proxy."
      }
    },
    {
      "@type": "Question",
      "name": "Where do client-side allowlists stop holding?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "A client-side allowlist — settings.json, hooks, any check that runs inside the agent's own process — holds against mistakes, which is most of what goes wrong. It does not hold against two things: string-matching misses semantics (a denied Bash rm rule doesn't catch the same deletion expressed as a script the agent wrote and then executed), and the client can opt out (anyone with write access to the client's settings file can remove the rule; --dangerously-skip-permissions itself removes prompts but not hooks). The fix is moving enforcement to a boundary the agent can't route around: a gateway that executes the tool calls, or a model proxy that strips denied tool_calls before they reach the harness."
      }
    }
  ]
}
</script>
