18,962 characters of skill listing left Claude Code's first request when we passed
--disallowedTools "Skill", and the request shrank by only 913 tokens, because the Workflow tool's description inlined the reference it normally loads from a skill and grew from 3,480 to 20,256 characters. In an empty directory on 2.1.285, the first request ofclaude -p "Reply with just OK."measured 23,207 input tokens by default and 2,664 with--strict-mcp-config --tools "", which left only the system prompt and a few short notes.
In an earlier lab, Six CLAUDE.md files, six codewords, two runs of the same configuration came back at 23,624 and 24,410 input tokens. When we compared the two transcripts, the only difference was the deferred tool list: one run included the tools of a remote connector from our claude.ai account and the other did not. We added --strict-mcp-config, the pairs started to match, and we went on to measure CLAUDE.md files.
That left an obvious question unanswered. If one connector's tool names are worth 786 tokens, what else is in the first request before any project file is involved, and which command-line flag removes which part? This article measures that baseline again on Claude Code 2.1.285 (claude --version), in a directory with nothing in it, and takes it apart one flag at a time.
The lab: an empty directory on a machine that is not empty
The lab is one empty directory under /tmp. It has no CLAUDE.md, no CLAUDE.local.md, no .mcp.json, no .claude/ folder, and it is not a git repository. Claude Code loads CLAUDE.md files from parent directories at launch, so we also walked every parent directory up to / and found none of those files there either.
The machine around it is not empty, and that matters for everything below. The user settings enable one plugin, a memory plugin that brings an MCP server with 15 tools and 21 skills. The claude.ai account behind the login has four connectors: two are connected and expose 38 tools between them, and two are waiting for sign-in. Nine more skills are synced from that account. The user settings also make auto the default permission mode, and auto memory is on. None of this lives in the lab directory. All of it showed up in the first request.
Every run used the same prompt and the same base flags, followed by the flags under test:
claude -p "Reply with just OK." --output-format stream-json --verbose \
--max-turns 1 --model opus --settings '{"disableAllHooks": true}'
The --settings override turns hooks off for the run, so the notification hook in the user settings stayed quiet. --model opus pins the model, which resolved to Opus 5.5, because the same text counts differently under a different tokenizer. We started the runs from inside another Claude Code session, so a small wrapper removed the environment variables that the parent session exports, such as CLAUDECODE, before calling claude. Without that step, the child process would have known it was running under another session.
We ran ten configurations, twice each, for 20 runs on 2026-09-30. In every pair, the second run's total matched the first to the token, and every reply was OK.
What we counted, and what the transcript records now
The number in every table below is the total input of the first request: input_tokens plus cache_read_input_tokens plus cache_creation_input_tokens from the usage block. With --max-turns 1 and a one-word answer, each run made exactly one request, so the result event of the stream and the first assistant message in the transcript under ~/.claude/projects/ carried the same three numbers. We read both. How the total split between cache reads and cache writes depended on what earlier runs had already cached. The sum did not.
On 2.1.285 the transcript records much more than usage. Each block of context that Claude Code adds around the first user message is saved as an attachment record, and its rendered field holds the text the model received, wrapped in <system-reminder> tags. The attachment types have readable names: environment, skill_listing, deferred_tools_delta, agent_listing_delta, mcp_instructions_delta and so on. There is also a prompt_snapshot record that holds the system prompt as a list of parts, plus every tool definition that went out with its schema. The CLI reference explains why something like it exists: "By default, Claude Code builds the system prompt once, on a conversation's first request, with the text from any system prompt flags applied, and records it in the session." We are reading that record, not documented output, and it may change.
So every piece of the request can be seen by name and measured in characters. The transcript does not say how many tokens each piece costs. For that, we removed pieces with flags and subtracted.
The 23,207 tokens, piece by piece
The stream's init event lists what the default session knew about: 78 tools, 5 MCP servers, 48 skills and 5 agent types. Not all of that was sent in full.
Eleven built-in tools went out with complete definitions: Agent, Bash, Edit, ListAgents, Read, ReportFindings, ScheduleWakeup, Skill, ToolSearch, Workflow and Write. Their descriptions alone add up to 14,590 characters, and serialized with their input schemas they come to 27,291. The two longest descriptions are Workflow at 3,480 characters and ScheduleWakeup at 3,148. Another 64 tools, 14 built-in and 50 from MCP servers, were sent as names only, under a line that tells Claude to load a schema through ToolSearch before calling one. The last three were connector tools that the same reminder announced as "ready to use". That accounts for all 78.
Everything else in the request is text. These are the attachments the first user message carried, measured in characters of rendered text:
| Attachment | What it holds | Characters |
|---|---|---|
skill_listing |
name and description of 48 skills | 18,962 |
deferred_tools_delta |
64 deferred names, 3 ready-to-use tools, a sign-in notice for 2 connectors | 3,156 |
mcp_instructions_delta |
server instructions from 1 connector | 2,053 |
agent_listing_delta |
5 agent types and their descriptions | 1,673 |
remote_session_change |
how to attribute git commits and pull requests | 579 |
session_context |
the signed-in account's email address | 568 |
auto_mode |
a note for the auto permission mode | 516 |
environment |
working directory, git or not, platform, shell, OS version | 364 |
model |
model name and knowledge cutoff | 161 |
total_tokens_reminder |
a token budget line | 86 |
date |
today's date | 64 |
The system prompt itself is 6,659 characters in 11 parts. The snapshot also records a one-line prefix for this kind of run: "You are a Claude agent, built on Anthropic's Claude Agent SDK." The largest part, 2,200 characters, is the auto memory section, which tells Claude where this project's memory directory is and how to write to it.
Measured in characters, then, the system prompt is the small part. The skill listing alone is almost three times its size, and the eleven tool definitions are bigger again.
Taking it apart one flag at a time
We removed the pieces in the order you would reach for them in a script: the account's connectors first, then every MCP server, then the two tools behind the largest listing, then everything except the file and shell tools, then those too. Each row lists the flags added to the base command.
| Flags added to the base command | First request | Change |
|---|---|---|
| none | 23,207 | |
ENABLE_CLAUDEAI_MCP_SERVERS=false (in the environment) |
20,707 | −2,500 |
--strict-mcp-config |
20,316 | −391 |
--strict-mcp-config --disallowedTools "Skill" "Workflow" |
11,161 | −9,155 |
--strict-mcp-config --tools "Bash,Read,Edit,Write" |
5,671 | −5,490 |
--strict-mcp-config --tools "" |
2,664 | −3,007 |
Each step removed something you can name. Turning off the claude.ai connectors removed their 35 deferred names, the three tools that were loaded in full, the sign-in notice for the two unconnected ones, and the 2,053-character instructions block: 2,500 tokens. --strict-mcp-config then removed the plugin's server, which was 15 deferred names with nothing else attached: 391 tokens. Disallowing Skill and Workflow removed both definitions, the 48-skill listing, and the one line of the system prompt that tells Claude to invoke /<skill-name> through Skill: 9,155 tokens. Narrowing the tools to Bash, Read, Edit and Write removed the Agent, ListAgents, ReportFindings, ScheduleWakeup and ToolSearch definitions, the 14 deferred built-in names, and the agent listing: 5,490 tokens. --tools "" removed the last four definitions along with the auto mode note and the commit attribution note: 3,007 tokens.
The steps add up: 2,500 + 391 + 9,155 + 5,490 + 3,007 + 2,664 = 23,207. Read them as one path through the request, not as independent prices. Removing the same pieces in another order gives other numbers, and the Skill section below shows an order that moves more than 8,000 tokens from one step to another.
Connectors: what --strict-mcp-config removed this time
This is where the codewords runs came apart, so it deserves a closer look. The CLI reference describes the flag in one sentence: "Only use MCP servers from --mcp-config, ignoring all other MCP configurations." We passed no --mcp-config at all, so the session was left with no MCP servers. The four connectors and the plugin's server were all gone, and the init event listed none.
ENABLE_CLAUDEAI_MCP_SERVERS=false is narrower. The environment variables page says: "Set to false to stop Claude Code from fetching claude.ai MCP servers. Enabled by default for logged-in users." With it set, the four connectors disappeared and the plugin's server stayed connected, which is how we could price the two separately. For a job that needs a local server but should never see the account's connectors, this is the switch. --strict-mcp-config with an explicit --mcp-config is the other route.
The plugin's 15 names cost 391 tokens, about 26 tokens per name. They added 735 characters to the deferred list, so these names ran at about 1.9 characters per token. For comparison, the plain English we appended with --append-system-prompt later in this article came to 3.3 characters per token. At 26 tokens a name, the 786-token gap from the codewords runs is what roughly 30 names would cost, and one of the two connected connectors here exposes exactly 30 tools. That fits, but it is arithmetic, not a measurement of the old runs.
Did the race come back? Not in these runs. Both default runs carried all four connectors, with the same two connected and the same two waiting for sign-in. In its entry for MCP_CONNECTION_NONBLOCKING, the environment variables page says: "In non-interactive mode (-p) without --input-format stream-json, Claude Code also waits for still-pending servers before the first turn regardless of this variable." Two matching runs do not prove that the race is gone. If identical runs ever differ by a few hundred tokens again, compare the deferred_tools_delta attachments of the two transcripts first.
Disallowing Skill saved 913 tokens
The skill listing is the largest single attachment, so for a script that never uses skills the obvious move is to take the Skill tool away. The CLI reference describes --disallowedTools like this: "Deny rules. A bare tool name removes the matching tools from Claude's context: "Edit" removes Edit, "*" removes every tool, and "mcp__*" removes every MCP tool."
We ran --strict-mcp-config --disallowedTools "Skill". The Skill definition left, and so did the 18,962-character skill listing. The first request went from 20,316 to 19,403 tokens. That is 913 tokens for removing a tool and a listing that together ran past 20,000 characters.
The transcript shows where the rest went. With Skill available, the Workflow tool's description contains this line:
With Skill disallowed, that line was replaced by a heading, # Workflow authoring reference, followed by 133 more lines: the reference itself, written into the tool description. The description grew from 3,480 to 20,256 characters. Most of what left with the skill listing came back inside Workflow. Only when we disallowed both tools did the request fall to 11,161 tokens, 9,155 below the strict baseline.
The documentation is accurate about the tool you name: Skill did leave Claude's context. What it does not say, and what we would not have guessed, is that another tool's definition gets rebuilt around the tools that remain. The rule we took from it is short. After you change the tool set, measure again, because definitions you did not touch can change too.
--tools takes the listings with it
The CLI reference describes the flag this way: "Restrict which built-in tools Claude can use. Use "" to disable all, "default" for the default set, or tool names like "Bash,Edit,Read"." It adds: "The flag doesn't affect MCP tools; to deny those too, use --disallowedTools "mcp__*"." That is why every --tools run above also carries --strict-mcp-config.
With --tools "Bash,Read,Edit,Write" the request was 5,671 tokens: four definitions and no listings. The init event still reported 48 skills and 5 agent types, but with no Skill tool and no Agent tool, neither listing was sent. The 14 deferred built-in names went too, and ToolSearch was not sent either.
--tools "" brought the request down to 2,664 tokens, and two more attachments disappeared with the last four tools: the auto mode note and the commit attribution note. Both were still there in the run that allowed only Bash, Read, Edit and Write, so each of them hangs on one or more of those four tools. We did not test which one.
What remains at 2,664 is the part no tool flag touches: the 6,523-character system prompt, the environment block, the model line, the date, the token budget line, the account note, and the prompt itself. One detail from that floor surprised us. The 2,200-character auto memory section was still in the system prompt, even though it tells Claude to save memories with the Write tool, which this session did not have.
The two largest built-in definitions are worth a look before you accept the default set in a script. In the transcript, ScheduleWakeup's description opens with "Schedule when to resume work in /loop dynamic mode", and Workflow's tells Claude to "ONLY call this tool when the user has explicitly opted into multi-agent orchestration." A one-shot claude -p call that summarizes a file or fixes a lint error needs neither of them. Unless the tool list says otherwise, both are sent in full with every request of the session.
What is yours and what is Claude Code's
Two flags remove customizations rather than tools, and between them they separate what this machine adds from what Claude Code brings on its own.
The CLI reference describes --setting-sources as a "Comma-separated list of setting sources to load (user, project, local)." The lab has no project or local settings, so --setting-sources project,local leaves only the --settings override from the command line. Combined with --strict-mcp-config, it took the request from 20,316 to 15,767 tokens, 4,549 fewer. The skill count dropped from 48 to 18. The plugin's 21 skills left with the plugin, and the 9 skills synced from the claude.ai account left as well, so on this machine they also come in through the user source. The default permission mode fell back from auto to default, which took the auto mode note with it, and the skill listing shrank from 18,962 to 6,332 characters.
--safe-mode cuts in a different direction. The CLI reference says it starts "with all customizations disabled to troubleshoot a broken configuration: CLAUDE.md, skills, plugins, hooks, MCP servers, custom commands and agents, output styles, workflows, custom themes, custom keybindings, status line and file-suggestion commands, LSP servers, and auto memory do not load. Authentication, model selection, built-in tools, and permissions work normally, which differs from --bare." We ran it without --strict-mcp-config, and it still dropped all five MCP servers, the claude.ai connectors included. It kept the same 18 skills that --setting-sources project,local had kept, and it removed the auto memory section, so the system prompt went from 6,659 to 4,459 characters. Permissions "work normally", and the permission mode stayed auto. The request was 15,239 tokens, the lowest we measured with every built-in tool still present.
So on this machine, roughly 15,200 of the 23,207 tokens come from Claude Code itself, and about 8,000 come from the parts that --safe-mode turns off: here, connectors, a plugin, synced skills and auto memory. Another machine will split differently, and a pair of --safe-mode runs is a cheap way to find out how.
--bare would have been the natural next row, and we could not run it. The headless page's example says to "Set ANTHROPIC_API_KEY before running it, because bare mode doesn't use your subscription login", and this machine only has a subscription login.
--append-system-prompt: 953 characters, 285 tokens
The last flag adds instead of removing. The CLI reference describes it as "Append custom text to the end of the default system prompt". We appended a 953-character, 170-word block of CI rules: ask no questions, never push, edit only src/ and test/, report failing tests by name, start the reply with PASS or FAIL. On top of --strict-mcp-config, the request grew from 20,316 to 20,601 tokens, 285 more.
In the snapshot, the text became the twelfth and last part of the system prompt. The second part of the recorded default prompt is the line __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__. The CLI reference describes that marker for custom prompts: Claude Code splits the prompt at that line and removes it, "so the part above it stays cached while the part below changes." The appended text sits well below it. The second run read all 20,599 cached tokens back, so a fixed appended text is cached like the rest of the prefix. We did not test a text that changes from call to call.
What we would pass to a scripted run
These are the choices we would make from the numbers above, for a claude -p call whose job is known in advance:
- If the job needs files and a shell and nothing else,
--toolswith an explicit list is the largest single cut, from 20,316 to 5,671 tokens here, because the skill and agent listings leave with the tools they belong to. - If the job needs skills, keep Skill and expect the listing to grow with every skill and plugin the user has.
--setting-sources project,localkeeps the user's plugins and synced skills out of a job that runs under that user's login. - Do not disallow Skill alone to save tokens. On 2.1.285 it saved 913. Disallow Workflow with it, or use
--tools. - Add
--strict-mcp-config, with an explicit--mcp-configif the job needs servers, so the account's connectors do not follow the job into CI.ENABLE_CLAUDEAI_MCP_SERVERS=falseremoves only the connectors.
The prefix is paid on every request, not once per session. In total_cost_usd, a repeat call that read the whole prefix from cache was reported at $0.0047 for the 23,207-token default and $0.0006 for the 2,664-token floor. The first default run, which wrote 12,755 tokens to the cache, was reported at $0.104.
What we did not measure
Everything here comes from one machine, one account, one model (Opus 5.5) and one version (2.1.285). A different model counts the same text differently, and a different account brings different connectors and synced skills.
The lab was not a git repository, so no git status was involved. The codewords lab was one, and its strict baseline with no CLAUDE.md was 18,442 tokens on 2.1.273, against 20,316 here. The two labs differ in more than the version, so we do not attribute that 1,874-token difference to the upgrade.
We did not run --bare, which needs an API key. We also did not run --system-prompt, --system-prompt-file, --exclude-dynamic-system-prompt-sections, --disallowedTools "mcp__*", or the disableClaudeAiConnectors setting, since we used the environment variable instead. We ran --setting-sources only together with --strict-mcp-config, so we cannot say whether leaving out the user source also keeps the connectors away.
Each configuration ran twice. The connector race from the codewords lab did not appear in two default runs, which is not evidence that it cannot happen.
The per-step token counts come from one path of removals. The Skill result shows that the pieces are not independent, so a different order would split the same 23,207 tokens differently.
The attachment and prompt_snapshot records are not documented. Their names and contents may change in any release, and every character count above depends on them.
All 20 replies were OK. We did not test whether removing a tool changed what Claude could do beyond answering one word. The 20 runs together were reported at $0.60 in total_cost_usd.
Reproduce it
LAB=$(mktemp -d) && cd "$LAB"
probe() { # probe <name> [flags under test...]
local name=$1; shift
claude -p "Reply with just OK." --output-format stream-json --verbose \
--max-turns 1 --model opus --settings '{"disableAllHooks": true}' "$@" \
> "$name.jsonl" < /dev/null
}
first_in() {
for r in "$@"; do
printf '%-8s %6s\n' "$r" "$(jq -r 'select(.type=="result") | .usage
| .input_tokens + .cache_read_input_tokens + .cache_creation_input_tokens' "$r.jsonl")"
done
}
probe default
probe strict --strict-mcp-config
probe tools4 --strict-mcp-config --tools "Bash,Read,Edit,Write"
probe tools0 --strict-mcp-config --tools ""
probe noskill --strict-mcp-config --disallowedTools "Skill"
probe noskillwf --strict-mcp-config --disallowedTools "Skill" "Workflow"
first_in default strict tools4 tools0 noskill noskillwf
To see the pieces, open the transcript of a run and list its attachments and the recorded tool descriptions:
sid=$(jq -r 'select(.type=="result") | .session_id' default.jsonl)
t=$(ls ~/.claude/projects/*/"$sid".jsonl)
jq -r 'select(.type=="attachment" and .rendered != null)
| [.attachment.type, (.rendered | map(.content // "") | join("") | length)] | @tsv' "$t"
jq -r 'select(.type=="attachment" and .attachment.type=="prompt_snapshot" and .attachment.tools != null)
| .attachment | "system prompt: \(.systemPrompt | map(length) | add)",
(.tools[] | "\(.name)\t\(.schema.description | length)")' "$t"
If you start the runs from inside another Claude Code session, unset the session variables it exports first, as described above. Your totals will not match ours, because they depend on your plugins, connectors and skills. The shape should look familiar: the tools and the listings they carry are most of the first request, and --tools is the flag that moves them.
Rulestack makes rules files, skills and hooks for Claude Code, sold at rulestack.gumroad.com. The measurement in this article needs nothing but claude, jq and an empty directory, so it is cheap to repeat after each Claude Code upgrade.
If your empty session splits differently, post your totals in the comments below, and follow @ai-shop.bsky.social for more first-request measurements like this one.


Top comments (0)