[BUG] disallowedTools in settings.json does not trim tool schemas, but --disallowedTools does

Status Open
Reported on v2.1.247
Maintainer reply None cached
Activity 0 comments · opened Aug 27, 2026

Preflight Checklist

  • [x] I have searched existing issues
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

disallowedTools in settings.json blocks the call but still sends the tool's schema, so it does not reduce context. The identically-named CLI flag --disallowedTools does remove the schema, and so does permissions.deny.

For Workflow the gap is 7,900 input tokens on every request.

--model opus, first API request of a headless run, summing input_tokens + cache_creation_input_tokens + cache_read_input_tokens. All rows taken back to back in one unchanged environment:

| config | input tokens | reclaimed |
|---|---|---|
| baseline | 23,052 | - |
| permissions.deny: ["Workflow"] | 15,152 | 7,900 |
| --disallowedTools Workflow | 15,152 | 7,900 |
| disallowedTools: ["Workflow"] (settings.json) | 23,052 | 0 |

The reclaimed column is the reproducible figure; absolute totals also carry the local CLAUDE.md, memory files and MCP definitions.

What Should Happen?

disallowedTools in settings.json should keep the denied tool's schema out of context, matching --disallowedTools and permissions.deny.

If the divergence is deliberate, documenting it would help. The shared name reads as a promise that the flag and the settings key do the same thing, and the settings key is the one an organization deploys fleet-wide.

Steps to Reproduce

m() { claude -p "ok" --model opus --output-format stream-json --verbose "$@" \
  | python3 -c 'import sys,json
for l in sys.stdin:
    try: d=json.loads(l)
    except: continue
    if d.get("type")=="assistant":
        u=d["message"]["usage"]
        print(u.get("input_tokens",0)+u.get("cache_creation_input_tokens",0)+u.get("cache_read_input_tokens",0)); break'; }

m                                              # baseline
m --settings '{"disallowedTools":["Workflow"]}' # unchanged
m --disallowedTools Workflow                   # -7,900
m --settings '{"permissions":{"deny":["Workflow"]}}'  # -7,900

Rows 2 and 3 deny the same tool by the same name and produce different prompt sizes.

Claude Model

Opus

Is this a regression?

I don't know

Claude Code Version

2.1.247

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Non-interactive/CI environment

Additional Information

Workflow is opt-in by design: its own tool description requires explicit user opt-in before it may be called. Its schema is the largest single item in the built-in tool block, so users who never invoke it still pay 7,900 tokens per request unless they find permissions.deny or the CLI flag.

Tool-search deferral already solves this class of problem and already applies to several built-ins (Monitor, WebFetch, Cron*, EnterPlanMode cost 0). Its scope looks MCP-only by design: CHANGELOG.md:4553 describes it as deferring "MCP tool descriptions" past a 10%-of-context threshold, and the toolSearchEnabled setting description likewise says "MCP tool schemas". Extending deferral to eagerly-loaded opt-in built-ins would make the default lean without per-org settings work.

View original on GitHub ↗