CCLearn

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`.

docs corroborate

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 excludeMechanismWhere 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 outputRead deny rulepermissions.deny in .claude/settings.json
Secrets and credentialsRead deny rule, plus the sandbox if it must be enforcedsettings, sandbox.filesystem
A subdirectory's CLAUDE.md you never want loadedclaudeMdExcludessettings, 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

json
{
  "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) and Read(**/.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 Read deny rule also blocks Edit and Write on the same path, including creating a new file there. If you deny Read(./src/generated/**) and later ask Claude to regenerate into it, it cannot. Two qualifiers the same paragraph carries: "NotebookEdit isn't covered, so add an Edit deny 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 a Read deny 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: NotebookEdit is simply not covered, which is why the explicit Edit deny is worth writing anyway.
  • Path rules are only consulted for Read(path) and Edit(path). Write Write(docs/**) or Glob(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. Use Edit(...) and Read(...).

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:

bash
# 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

  1. Delete the .claudeignore. Leaving it there tells the next person on the repo that exclusions are handled.
  2. Check what is actually in .gitignore — for most of what people list, that is already the answer.
  3. Add Read deny 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 answer

    That split is yours to read off your own repo, but its shape is predictable: content searches respect .gitignore by default, so node_modules/, dist/, build/ and anything else already ignored there needed nothing at all. That overlap is half the reason a .claudeignore feels 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 Read deny 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 answer

    It cannot write there. A Read deny 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.

    NotebookEdit is not covered at any version, which is why an explicit Edit deny 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 answer

    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.

Where this came from

Official pages that back this