Your dependencies ship a CLAUDE.md, and Claude Code reads it
6m read time

Your dependencies ship a CLAUDE.md, and Claude Code reads it

I audited every instruction file that could steer Claude Code across 40 repositories on my machine. In 14 of them, the files nobody would think to check were written by package maintainers, sitting in node_modules and vendor, and Claude Code loads them the moment it reads a file next to them.

I ran an audit over every git checkout on my machine: 40 repositories, each asked one question. Which instruction files could steer a Claude Code session started here?

Most of the answers were boring. My own rules, a project CLAUDE.md, the occasional path-scoped rule.

The interesting rows were files I never wrote. Eight repositories had this one sitting in node_modules/bun-types/CLAUDE.md:

text
Default to using Bun instead of Node.js.

- Use `bun test` instead of `jest` or `vitest`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.

That is a perfectly reasonable file for the people who build Bun. It ships to everyone who installs Bun's type definitions, including projects that run on Node and test with Vitest.

One Read in node_modules/bun-types pulls its CLAUDE.md into context past .gitignore, until claudeMdExcludes blocks it

What the audit counted ​

The audit is Kanon 0.0.26, an open-source tool that lists every instruction file a session in a given checkout could load, and marks the ones that come from somewhere other than you as foreign. It reads the file system. No session, no model, no tokens.

Across the 40 checkouts:

  • 19 had nothing beyond my five user-level rule files. No project instructions at all.
  • 14 carried at least one CLAUDE.md written by a dependency's maintainer: 24 files from seven packages.
  • 8 of those 14 had no project instruction file of their own. In those repositories, the only project-level instructions that could load were written by a stranger.
PackageReposStill in the latest release?
comment-parser9no, gone in 1.4.9
bun-types8yes, 1.4.4
thread-stream2no, 4.2.0 ships a .claude/settings.local.json now
ts-loader1yes, 9.6.2 ships a CLAUDE.md and an AGENTS.md
nodemailer1no
javascript-obfuscator1no
phpstan/phpstan1(vendored through Composer, not checked)

Several maintainers cleaned up already. Your lockfile does not care: those repositories still pin the versions that carry the file, and they will until someone runs an update.

Does Claude Code actually load them? ​

The documentation says so, in one sentence most people skim past. Claude Code discovers CLAUDE.md files in subdirectories and "loads each one once Claude reads, writes, or edits another file in that subdirectory." Nothing in that rule treats node_modules differently.

I checked anyway. A throwaway repository, bun-types installed, Claude Code 2.1.312, one prompt: read node_modules/bun-types/package.json and report the version. The answer was 1.4.4, and the session transcript shows the Bun file attached to the conversation right after the read, all of it. The run cost under one cent.

Then the question you are probably asking: surely node_modules is gitignored, so Claude stays out?

Gitignore only shapes what Claude finds, and even the finding is patchy. Grep skips gitignored files. Glob does not: per the tools reference it "finds gitignored files alongside tracked ones."

I added node_modules/ to .gitignore and ran the same prompt. The file loaded again. The moment Claude reads one file in that directory, say to check a type signature while debugging or to see how a library parses its options, the maintainer's instructions come along.

Does it change what the agent does? ​

Whether the agent acts on a loaded file is a separate question, so I measured that too. Two small sets on Sonnet, five runs each way. Every run first read one package.json inside node_modules: bun-types for the test arm, comment-parser 1.4.9, which no longer ships a CLAUDE.md, for the control. The first set ran with my normal user settings, the second with project settings only.

In the first set, the project decided the answer, and nothing happened. With "test": "vitest run" in package.json, all ten runs answered npm test. One Bun run even said it was overriding the bundled file because the project said otherwise.

In the second set, the project left the question open, and the file got through. Asked which HTTP library and test runner to add to what the prompt called a Node project, with neither installed, the control picked Fastify and Vitest five times out of five. In the Bun arm, one run answered Bun.serve() and bun test, and said it was following "the guidance bundled with the installed bun-types". Three of the other four named the file and explained why they ignored it, mostly because the prompt said Node.

Five runs prove that the path exists. They are too few for a rate, so I am not quoting one. What they show is the shape: a dependency's instructions lose to a clear project signal and win, sometimes, where you never wrote one down. And the open questions are exactly where you let the agent choose.

Rules not followed, or rules you never saw? ​

When Claude Code ignores your CLAUDE.md, the usual advice is to write the rule more clearly. The documentation adds a warning that deserves more attention than it gets: "if two instructions contradict each other, Claude may pick one arbitrarily."

You cannot resolve a contradiction you do not know about. Your CLAUDE.md file says Vitest, a dependency's file says bun test, and the session that reads the wrong directory has both in context. From the outside that looks like the agent not following rules. It is following someone's.

bun-types writes honest advice for Bun users. The mechanism does not check intent, though. Whatever a maintainer puts in that file, or whoever takes over a maintainer's account, reaches every agent that opens a file in the package. It is the same trust problem as the supply chain behind MCP servers, with an install step you never think of as one.

What to do about it ​

Exclude dependency directories. Claude Code has a setting for exactly this, claudeMdExcludes, matched against absolute paths with glob patterns:

json
{
  "claudeMdExcludes": ["**/node_modules/**", "**/vendor/**"]
}

Put it in your user settings and it covers every project. I added the node_modules pattern to the throwaway repository and ran the read again: the version came back, the Bun file did not load. Per the documentation, the patterns apply to a subdirectory's AGENTS.md as well.

Audit what could load. Kanon's audit lists every file per checkout and flags the foreign ones. Claude Code's own /context shows what loaded at launch. A subdirectory file announces itself with a Loaded line in the terminal only once it is already in context, so neither warns you in advance. The InstructionsLoaded hook logs every CLAUDE.md and rule file that loads, with the reason, which is the right tool when you want proof of what a past session had in context.

Write down the decisions you care about. The dependency's file got through where my project was silent. If the test runner or the HTTP framework matters to you, a line in your own CLAUDE.md turns an open question into a clear signal, and a clear signal beat the bundled file ten times out of ten.

In eight of my 40 repositories, the Bun team had written an instruction file my sessions could load. They never meant it for me, and until this week I never knew it was there.