There is no .claudeignore file
`.claudeignore` is not a Claude Code feature — it is inert. The documented ways to keep Claude out of a path are `.gitignore` (already respected by content searches) and `Read` deny rules in `.claude/settings.json`.
The official documentation describes the mechanism this relies on. The technique itself is community practice.
What this costs you if it is wrong
Deny rules are a context-hygiene tool, not a security boundary. They cover Claude's file tools and the file commands Claude Code recognizes in Bash, but not arbitrary subprocesses that open files themselves — a script Claude runs can still read a denied path. For real secrets, use the sandbox's filesystem restrictions, or keep the file off the machine. On v2.1.208 and later a too-broad Read deny also silently blocks Edit on the same paths, and from v2.1.228 Write as well, which reads as Claude being mysteriously unable to touch a file.
You added a .claudeignore to the repo root — node_modules/, dist/, *.lock, the vendored SDK — and nothing complained. No startup warning, no invalid-settings dialog, no mention of it in claude doctor. Then you watched a session read straight through vendor/ anyway.
The file does nothing. It is not a Claude Code feature, and the silence is the whole problem: an unrecognized dotfile at the repo root is just a file, so there is nothing for Claude Code to reject.
What the docs actually document
Grep the official documentation set for claudeignore and you get zero hits across every page. What the docs do describe, in Reduce what Claude reads, is two mechanisms that split the job:
| You want to exclude | Mechanism | Where it lives |
|---|---|---|
Generated/vendored paths already git-ignored (node_modules/, dist/, build/) | Nothing — content searches respect .gitignore by default | .gitignore |
| Checked-in vendored code, committed generated output | Read deny rule | permissions.deny in .claude/settings.json |
| Secrets and credentials | Read deny rule, plus the sandbox if it must be enforced | settings, sandbox.filesystem |
A subdirectory's CLAUDE.md you never want loaded | claudeMdExcludes | settings, any scope |
Half the reason .claudeignore feels like it works is that first row. The paths people put in it are usually already in .gitignore, so Claude's searches were skipping them regardless — the fake file gets credit for what git was doing.
The real mechanism
{
"permissions": {
"deny": [
"Read(./vendor/**)",
"Read(./src/generated/**)",
"Read(./.env)"
]
}
}Put that in .claude/settings.json to cover everyone in the repo, .claude/settings.local.json for just you, or user settings for every project on the machine. Deny rules are evaluated before ask and before allow, from any scope, so a project-level deny is not something an allow rule elsewhere can undo.
Three details the docs are explicit about and that catch people:
- Read and Edit rules use gitignore pattern syntax, so a bare filename matches at any depth —
Read(.env)andRead(**/.env)are the same rule. That is a genuine similarity to the file people were trying to write, which is probably how the myth survives. - A
Readdeny rule also blocks Edit and Write on the same path, including creating a new file there. If you denyRead(./src/generated/**)and later ask Claude to regenerate into it, it cannot. Two qualifiers the same paragraph carries: "NotebookEdit isn't covered, so add anEditdeny rule for paths no tool may change," and "The check requires Claude Code v2.1.208 or later on edits, and v2.1.228 or later on writes." Before v2.1.208 aReaddeny was a read rule and nothing more, and until v2.1.228 it stopped edits but not writes — so a settings file written on an older build may be quietly counting on writes the same rule now blocks. The notebook hole is separate and carries no version at all:NotebookEditis simply not covered, which is why the explicitEditdeny is worth writing anyway. - Path rules are only consulted for
Read(path)andEdit(path). WriteWrite(docs/**)orGlob(docs/**)and Claude Code accepts the rule, never uses it, and warns at startup — a behaviour whose paragraph ends "Requires Claude Code v2.1.210 or later", so an older build may leave you no warning at all. UseEdit(...)andRead(...).
What is unverified here
Nobody has run this. The claim rests on two things: the documentation set never mentions .claudeignore on any of its 77 pages, and it documents a different mechanism for the exact job — which is strong evidence about the current supported surface, but an absence, not a statement. It does not rule out that some build once read such a file, and it is not a promise about a future one.
The originating source is a newsletter author retracting her own earlier advice, which is secondary. Treat that as motivation to check, not as proof.
Settle it yourself in about thirty seconds:
# in a scratch directory, not your real repo
mkdir -p /tmp/ignoretest && cd /tmp/ignoretest
printf 'canary.txt\n' > .claudeignore
printf 'the canary is alive\n' > canary.txt
claude --print 'read canary.txt and tell me exactly what it says'If the phrase comes back, .claudeignore did nothing. Then do the contrast run — add .claude/settings.json with "deny": ["Read(./canary.txt)"] and repeat. The second run should refuse, and /permissions in an interactive session should list the rule. The .claudeignore never appears in /permissions or claude doctor at all, which is the tell: real configuration is visible to the tooling that reports configuration.
What to do now
- Delete the
.claudeignore. Leaving it there tells the next person on the repo that exclusions are handled. - Check what is actually in
.gitignore— for most of what people list, that is already the answer. - Add
Readdeny rules for the checked-in paths git does not cover, and confirm they show up in/permissions.
Checkpoint
-
Which of your intended exclusions were already covered by
.gitignore, and which needed a deny rule?Show answerAnswer
That split is yours to read off your own repo, but its shape is predictable: content searches respect
.gitignoreby default, sonode_modules/,dist/,build/and anything else already ignored there needed nothing at all. That overlap is half the reason a.claudeignorefeels like it works — the inert file takes credit for what git was already doing.What is left over is the real list: checked-in vendored code, committed generated output, and secrets. Those need
Readdeny rules in.claude/settings.json, and you confirm each one by checking it appears in/permissions. -
If you deny
Read(./generated/**), what happens the next time you ask Claude to write a file into that directory?Show answerAnswer
It cannot write there. A
Readdeny also blocks Edit and Write on the same paths, including creating a new file, so the regeneration you asked for fails and reads as Claude being mysteriously unable to touch the path. The check is version-gated — v2.1.208 or later on edits, v2.1.228 or later on writes — so a settings file authored on an older build may be quietly relying on writes the same rule now stops.NotebookEditis not covered at any version, which is why an explicitEditdeny is still worth adding for paths no tool may change. -
A script Claude runs with Bash opens a denied path with Python's
open(). Does the deny rule stop it?Show answerAnswer
No. Deny rules cover Claude's file tools and the file commands Claude Code recognizes in Bash —
cat,head,tail,sed, redirect targets — but not an arbitrary subprocess that opens the file itself. They are context hygiene, not a security boundary.For anything that has to be enforced, that is what the sandbox's filesystem restrictions (
sandbox.filesystem) are for; for real secrets, keep the file off the machine.