aieveryminute

A 7.9MB node_modules cost 2 tokens, and .claudeignore does nothing

Cost guides tell you to add a .claudeignore because Claude Code otherwise reads node_modules when it indexes your repo. Nothing gets indexed into context: an identical project with 1,960 extra files cost two more tokens at startup. And the file they recommend does not block anything, which a canary in an excluded directory proves by coming straight back.

The advice is specific and it comes with a mechanism: “Add a .claudeignore to prevent Claude Code from reading unnecessary files. Without .claudeignore, Claude Code reads node_modules, dist folders, and binaries when indexing.”

Both halves are checkable. Does repository size cost anything, and does that file do anything.

A 7.9MB node_modules cost two tokens

Two identical projects, one carrying a synthetic node_modules of 1,960 files and 7.9MB across 140 packages. The prompt asks for nothing at all: Reply with only READY. If Claude Code indexed the repository at startup, the bloated project would have to cost more.

Startup probe Context tokens, in round order
Lean project 22,401, 22,401, 22,401, 22,401
Plus 7.9MB node_modules 22,403, 22,403, 21,881, 22,403
Plus node_modules and a .claudeignore 22,411, 22,411, 22,411, 22,411

The lean arm returned the same number four times. Adding 1,960 files moved it by 2 tokens, and the third bloated round came in 520 below the lean baseline, which is the direction that cannot be explained by reading more. Zero tool calls in all twelve startup runs.

No indexing step reaches the context. That is the exact claim the measurement supports, and it is worth keeping exact: this corpus records context tokens and the model’s own tool calls, neither of which can see what the CLI touches on disk before the first turn. Whatever it does there, it does not put your repository into the context window. So repository size is not a startup cost, and it cannot become one by excluding anything.

In 24 runs it never touched node_modules

The startup probe is the strict test. The realistic one is a prompt that invites looking: What does this project do? That ran on all three arms too.

Across all 24 token runs, including the 12 exploring ones, the number of tool calls that touched node_modules was zero.

What it did instead varied, and the variation is the point. All twelve listed the directory and read README.md and package.json. Five went on to open a source file, three of those reading all three. Seven opened no source file at all. In the ignored arm it also opened the .claudeignore itself, four rounds out of four. It never once opened node_modules, so there was nothing for a .claudeignore to prevent.

The explore probe cannot say more than that, and it is worth being blunt about why. Its turn counts came out lean [5, 5, 6, 8], bloated [5, 5, 5, 5], ignored [3, 6, 9, 9], and context follows those rather than the arm. On medians the bloated arm reads 14.6% below the lean one, which is not an effect, it is noise, and read the other way round it would be an equally false finding. That probe is underpowered by design and only the count above survives it.

The file is inert, and a canary proves it

No measurable effect is consistent with two different worlds: the file is honoured but there was nothing to exclude, or the file does nothing. Only a direct test separates them.

A file node_modules/pkg-000/SECRET.txt containing CANARY-7731, a .claudeignore naming node_modules/, and then: read that file and reply with only its contents. Three rounds per arm.

Asked to read a file inside node_modules Canary returned
No protection 3 of 3
.claudeignore naming node_modules/ 3 of 3
settings.json deny rule Read(./node_modules/**) 0 of 3

.claudeignore did not block the read, did not hide the directory from a listing, and behaved exactly like having no file at all. The documented mechanism blocked it every time and said so plainly, in its own words: it could not read the file because the path is “blocked by your permission settings”.

The deny arm is also the control that makes the null result mean anything. Every call ran with --setting-sources project, and a null from .claudeignore would be worthless if project-level configuration were not being loaded in the first place. The deny rule lives in exactly that scope, in .claude/settings.json, and it took effect 3 times out of 3. So project config was loaded and obeyed in the same sessions where .claudeignore was ignored.

Searching the official documentation index agrees. The string claudeignore appears in 0 of the 193 documented pages. CLAUDE.md appears in 8, which is the control proving the search ran rather than the fetch coming back empty.

What to do instead

If you want a directory genuinely off limits, the deny rule is the thing that works, in .claude/settings.json:

{
  "permissions": {
    "deny": ["Read(./node_modules/**)"]
  }
}

That is worth having for secrets. It is not worth having for cost, and this is the part the guides get backwards: there is no cost to save. The bill is not driven by what is sitting in your repository, it is driven by what gets read, and this site measured what actually predicts it: round trips, at roughly 23,000 tokens each. A directory nobody opens is free.

What this does not settle

One project shape, one bloat size. 7.9MB and 1,960 files. Nothing that scales with file count reaches the context at startup, so the result should hold at any size, but that last step is an inference beyond what was measured.

Whether the file is read at all is not settled, only whether it does anything. Adding a .claudeignore costs a consistent +10 tokens against lean, in 4 of 4 rounds, and +8 against the bloated arm. Its body is 26 bytes, which is roughly 8 tokens, so the numbers are equally consistent with Claude Code reading the file and disregarding it, or with the filename simply appearing in a directory listing. This corpus cannot separate those, and it does not need to: either way the file changes no behaviour.

The canary is a capability test, not a frequency test. It asks whether a file can be read when requested directly. How often the model would wander into node_modules unprompted is a different question, and the 24 token runs answer it with zero.

The deny rule was tested for reads only here, and has since been measured properly. How far a Read deny rule actually reaches puts it through eight routes: it covers the Read tool and the shell commands Claude Code parses, and it does not cover a program that opens the file itself.

Controls

The bloat was measured on disk every run and published: files, megabytes, and the node_modules share of both. Without that, “no effect” and “no node_modules” look identical.

The .claudeignore was written and its sha256 recorded per run, so the null result cannot be an unwritten file. Each canary round separately re-checked that the secret was on disk and that the arm’s config file existed before asking.

Tool calls were captured with --output-format stream-json, so “never touched node_modules” is an observation from the action record rather than an inference from token counts.

Isolation on every call, --setting-sources project, asserted onto the command line. Tokens from modelUsage, not .usage.

Every run is published in claudeignore-and-node-modules.json.

POSTaieveryminute.com#context-costbuilt 2026-08-31 17:47 UTC