Tutorials
Sofia Nieves9 min read4 views

Claude Agent SDK: tools vs allowedTools, and what a subagent definition silently accepts

allowedTools auto-approves, tools restricts. That half is documented. What is not: an AgentDefinition has no allowedTools field, and the SDK forwards one verbatim to the CLI anyway, unvalidated.

Flat dark diagram with two lanes: the upper blue lane passes through a hexagonal transformation node between its start and end, while the lower amber lane runs straight from start to end with no node, showing one level that translates a value and one that passes it through unchanged.
Flat dark diagram with two lanes: the upper blue lane passes through a hexagonal transformation node between its start and end, while the lower amber lane runs straight from start to end with no node, showing one level that translates a value and one that passes it through unchanged.
On this page

Quick answer (September 2026). In @anthropic-ai/claude-agent-sdk, allowedTools does not restrict anything. It auto-approves. The option that controls which built-in tools exist is tools. That much is documented and has been written up elsewhere. What is not documented is what happens one level down: an AgentDefinition has no allowedTools field at all, and if you write one anyway the SDK forwards it to the CLI verbatim, unvalidated, along with typos and shapes the type system forbids. TypeScript catches it. Plain JavaScript and JSON config do not. Measured on SDK 0.3.274.

Anthropic TypeScript npm Node.js

The part that is already written down

Credit where it is due, because this half is not new and you should not learn it from me.

Anthropic's own type definitions say it plainly. From sdk.d.ts in the published package:

allowedTools, verbatim: "List of tool names that are auto-allowed without prompting for permission. These tools will execute automatically without asking the user for approval. To restrict which tools are available, use the tools option instead."

The confusion this causes is well travelled. Issue 20242 on anthropics/claude-code, opened 23 January 2026 and closed as not planned, is a docs bug report that lays out the security consequence precisely: a developer writing allowed_tools=["Bash", "Edit"] may believe the agent is now limited to those two, when in fact every tool is still available and those two now run without a prompt. Issue 19 on the TypeScript SDK reports the same surprise from the other direction, and was closed with no answer at all.

It has also been covered by other writers. A Claude Agent SDK guide published on 16 September 2026 states the split directly, and a pitfalls post has a section headed "API Confusion" whose author says outright that they do not know what disallowedTools is for.

So the split is known. This tutorial starts where those stop: at the wire, and at the subagent.

What each shape actually sends

The SDK is a wrapper. It spawns the claude binary and passes your options as command line flags, so you can see exactly what it decided by capturing the argument vector. Four shapes of the tools option, measured:

Scroll to see more

You writeThe SDK sends
tools omittedno --tools flag at all
tools: ['Bash', 'Read']--tools "Bash,Read"
tools: []--tools ""
tools: { type: 'preset', preset: 'claude_code' }--tools "default"

Two things worth pausing on.

The preset name does not survive. You write claude_code; the flag says default. That is not a bug, it is a translation, and the CLI's own help text is where it is defined. The claude binary ships inside the SDK package, so you can read the contract first hand:

--tools
    Specify the list of available tools from the built-in set.
    Use "" to disable all tools, "default" to use all tools,
    or specify tool names

So tools: [] sending an empty string is the documented way to say "no built-ins", and the preset sending default is the documented way to say "all of them". The SDK shapes map cleanly onto a CLI contract that was already public.

Omitting tools is not the same as passing the preset. One sends no flag; the other sends --tools default. Whether those two end up equivalent is decided inside the CLI, which is past where this harness can see. I am reporting what is sent, not what is resolved.

While you are here: the CLI accepts --allowedTools and --allowed-tools as aliases. The SDK only ever emits the camelCase form, which is worth knowing if you are grepping a process list.

The part nobody has written up: the subagent level

Now define a subagent. Here is the whole of what AgentDefinition accepts for tools, from the same type file:

  • tools?: string[] , documented as "Array of allowed tool names. If omitted, inherits all tools from parent."
  • disallowedTools?: string[]

That is the complete list. There is no allowedTools on an AgentDefinition. A subagent cannot carry its own auto-approval list; approval still comes from the top level.

And read that JSDoc again. The subagent's availability field is described as "allowed tool names". So the word "allowed" means availability one level down and approval one level up, in the same file. The naming collision that issue 20242 complained about is not just between two options, it is between two levels.

What TypeScript catches

If you are writing TypeScript with strict on, the compiler saves you. Both of these fail to compile:

error TS2561: Object literal may only specify known properties,
but 'allowedTools' does not exist in type 'AgentDefinition'.
Did you mean to write 'disallowedTools'?

error TS2353: Object literal may only specify known properties,
and 'type' does not exist in type 'string[]'.

The first is the one to read twice. You meant "approve these tools". The compiler's suggested fix is disallowedTools, which removes them. Accept that quick fix without thinking and you have inverted your own intent while making the error go away. The second says the tools preset shape is valid on Options and not on an AgentDefinition, whose tools really is string[] only.

What the SDK does not catch

Subagent definitions do not travel as CLI flags. They go over the control protocol on standard input, as JSON, in the initialize request. Which means TypeScript is the only thing checking them, and if you are not using TypeScript nothing is.

Measured, all three of these are transmitted to the CLI exactly as written:

{"description":"d","prompt":"p","allowedTools":["Bash"]}
{"description":"d","prompt":"p","tools":{"type":"preset","preset":"claude_code"}}
{"description":"d","prompt":"p","toolz":["Read"]}

A field that does not exist in the type. A shape the type forbids. A plain typo. No error, no warning, no stripping. The SDK adds no runtime validation of its own here.

That gives the asymmetry this tutorial is really about. The same value is treated differently depending on which level you put it on. Put { type: 'preset', preset: 'claude_code' } on Options.tools and the SDK translates it to --tools default. Put the identical object on AgentDefinition.tools and it is forwarded as a raw object. One level has a translation step; the other is a pass through.

The practical consequence: a JavaScript project, or one that builds agent definitions from a JSON or YAML config file, can ship allowedTools inside a subagent forever. It will not throw. It will simply never do anything, because the field is not part of the contract the CLI reads.

Reproduce it yourself

No API key and no billing needed. The trick is to point the SDK at a fake CLI that records what it was given and exits.

npm init -y
npm install --save-exact @anthropic-ai/claude-agent-sdk

Save this as fake/claude.cjs and chmod +x it:

#!/usr/bin/env node
const fs = require('node:fs');
fs.writeFileSync('/tmp/shipped/argv.txt', process.argv.slice(2).join('\n'));
let buf = '';
process.stdin.on('data', function (chunk) { buf += chunk; });
setTimeout(function () {
  fs.writeFileSync('/tmp/shipped/stdin.txt', buf);
  process.exit(0);
}, 2000);

Then probe.mjs:

import { query } from '@anthropic-ai/claude-agent-sdk';
import { readFileSync } from 'node:fs';

const FAKE = '/tmp/shipped/fake/claude.cjs';

async function capture(options) {
  try {
    const q = query({ prompt: 'hi', options: { ...options, pathToClaudeCodeExecutable: FAKE } });
    for await (const message of q) { void message; }
  } catch (err) { void err; }
  return {
    argv: readFileSync('/tmp/shipped/argv.txt', 'utf8').split('\n'),
    stdin: readFileSync('/tmp/shipped/stdin.txt', 'utf8'),
  };
}

function toolsFlag(argv) {
  const i = argv.indexOf('--tools');
  return i === -1 ? '(flag absent)' : JSON.stringify(argv[i + 1]);
}

const preset = { type: 'preset', preset: 'claude_code' };

console.log('omitted   ', toolsFlag((await capture({})).argv));
console.log('array     ', toolsFlag((await capture({ tools: ['Bash', 'Read'] })).argv));
console.log('empty     ', toolsFlag((await capture({ tools: [] })).argv));
console.log('preset    ', toolsFlag((await capture({ tools: preset })).argv));

const agent = { description: 'd', prompt: 'p', allowedTools: ['Bash'] };
const result = await capture({ agents: { reviewer: agent } });
for (const line of result.stdin.trim().split('\n')) {
  const parsed = JSON.parse(line);
  if (parsed.request && parsed.request.agents) {
    console.log('agent sent', JSON.stringify(parsed.request.agents.reviewer));
  }
}

On SDK 0.3.274 that prints:

omitted    (flag absent)
array      "Bash,Read"
empty      ""
preset     "default"
agent sent {"description":"d","prompt":"p","allowedTools":["Bash"]}

Adjust the paths to wherever you put the files. Point capture at anything else you want to check; the pattern generalises to any option you are unsure about.

What to actually do

  • To limit what the agent can touch, use tools. That is the availability control at the top level, and tools on an AgentDefinition at the subagent level.
  • To stop the prompting, use allowedTools. Top level only. It cannot restrict, and a subagent has no equivalent.
  • To remove something outright, use disallowedTools. It exists on both levels.
  • Keep strict on. It is the only automatic check that a subagent definition gets, and TS2561 is the error you want to see. Read its suggestion rather than accepting it.
  • If your agent definitions come from JSON or YAML, validate them yourself. Nothing else will.

For the wider picture of defining and delegating to subagents, see the Claude Agent SDK subagents tutorial. For how MCP tool names interact with approval, see custom tools in the Claude Agent SDK.

Scope and limits

Everything above is a measurement of what the SDK sends: the argument vector, and the JSON written to the CLI's standard input. All of it is reproducible with the harness above, on version 0.3.274, with no API key.

What is deliberately not claimed here is what the CLI and the model then do with those values. Whether --tools default and an absent flag resolve identically, how a subagent's tools list composes with a parent's, and what the CLI does when it receives an allowedTools key it does not expect, are all decided downstream of this boundary. Answering them needs a real Claude Code session and a billed run. Where this tutorial says "sent", it means sent.

Sofia Nieves

Written by

Sofia Nieves

Sofia works on agent evaluation and reliability. She writes about measuring LLM systems before and after they reach production.

Frequently asked questions

Does allowedTools restrict which tools a Claude Agent SDK agent can use?

No. It auto-approves them. Anthropic's own type definitions state that allowedTools lists tools that are auto-allowed without prompting, and that you should use the tools option instead if you want to restrict availability. Setting allowedTools to Bash and Edit leaves every other tool available and simply stops those two from prompting.

What does tools set to an empty array do in the Claude Agent SDK?

It sends the flag --tools with an empty string, which the CLI documents as disabling all built-in tools. Measured on SDK 0.3.274. Your own MCP tools are unaffected, so this is the way to run an agent on nothing but the tools you supply.

What does the tools preset claude_code serialise to?

It becomes --tools "default". The preset name claude_code does not appear in the flag. The CLI's own help text defines "default" as meaning all built-in tools. Omitting the tools option entirely is different again: no --tools flag is sent at all.

Can an AgentDefinition have its own allowedTools in the Claude Agent SDK?

No. AgentDefinition accepts tools and disallowedTools only. There is no allowedTools field on a subagent, so a subagent cannot carry its own auto-approval list. In TypeScript this produces error TS2561, 'allowedTools' does not exist in type 'AgentDefinition'. Note the compiler suggests disallowedTools, which does the opposite of what you meant.

Does the Claude Agent SDK validate subagent definitions?

No. Subagent definitions are sent over the control protocol on standard input as JSON, and measurement on version 0.3.274 shows an allowedTools field, a preset object where the type requires a string array, and an outright typo such as toolz are all forwarded verbatim with no error or warning. TypeScript is the only thing that checks them, so plain JavaScript and JSON or YAML config files get no protection at all.

Is the CLI flag --allowedTools or --allowed-tools?

The CLI accepts both as aliases. The SDK only ever emits the camelCase form, --allowedTools, so if you are inspecting a running process or grepping a process list, that is the spelling you will find.