← Back to Home
Claude Code · Security

.claudeignoreWhy the idea matters even though Claude Code doesn't officially support it

I spent an hour this week trying to figure out why Claude kept opening node_modules during a refactor. Turns out the answer is simple and slightly annoying: there is no native .claudeignore file in Claude Code. .gitignore is partially honored. Everything else is on you. Here is what I learned setting it up properly, and why you cannot afford to skip this.

01
What people mean by .claudeignore
The mental model borrowed from git
The intuition

If you have used Git for more than a week, you know .gitignore. You drop a file at the root of your repo, list patterns like node_modules/, dist/, .env, and Git pretends those paths do not exist. Clean diffs, no accidental commits.

.claudeignore is the same idea, transplanted onto your AI agent. You write the file, list the patterns you do not want Claude poking at, and the agent leaves them alone. Simple. Obvious. Necessary.

The catch

It does not exist. Not as a first-class feature in Claude Code. The official docs make no mention of .claudeignore. There is a long-running GitHub issue (#79 on the anthropics/claude-code repo) asking for it, and it is still open. Anthropic's answer so far is "use the permissions system instead."

Which is fine, except that nobody told the rest of the internet. Half the blog posts you will read still tell you to create a .claudeignore. The file you create will sit there doing absolutely nothing.

02
Why this matters
Three reasons context hygiene is not optional
Reason 01
Tokens cost money

When Claude reads a 3MB package-lock.json "just to check", that is real tokens on your bill. On a Max plan you hit the rate limit faster. On API, you pay cash. I have watched a single careless Grep across node_modules burn through 40k tokens.

Reason 02
Noise hurts accuracy

Even with a 1M context window, junk in the context degrades output. When Claude grep's your dist/ folder and finds 200 minified matches for a function name, it has to filter them out before answering. Sometimes it does not.

Reason 03
Secrets leak

The real one. Your .env file with the Stripe live key. Your ~/.ssh/id_rsa. An aws-credentials dump from last quarter. Claude can read them. If you are piping responses anywhere, you have to assume those secrets just left the building.

The Register, January 2026

A reporter wrote a .claudeignore file blocking .env, asked Claude what was inside, and Claude read it anyway. Anthropic's response was essentially "yes, that file is not a real thing, please use the permissions system." If you have been counting on .claudeignore alone, your secrets are not protected.

03
What actually works
settings.json deny rules
The real ignore file

Claude Code's permissions system lives in .claude/settings.json (project) or ~/.claude/settings.json (global). It supports an permissions.deny array where you list tool calls Claude is not allowed to make. Patterns follow the same gitignore-style syntax you already know.

.claude/settings.json
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(**/*.pem)",
      "Read(**/*.key)",
      "Read(~/.ssh/**)",
      "Read(**/node_modules/**)",
      "Read(**/dist/**)",
      "Read(**/build/**)",
      "Read(**/.next/**)",
      "Read(**/coverage/**)",
      "Read(**/*.log)"
    ]
  }
}

Drop this into .claude/settings.json at the root of your project and commit it. Now when Claude tries to Read any of those paths, the tool call is blocked before it even runs. No token spend. No secrets in context.

Two precedence rules to remember
Deny wins
Rules are evaluated deny first, then ask, then allow. The first matching rule fires. So even if a parent setting allows Read(**), a project-level Read(./.env) deny will still block it.
Settings cascade
Claude Code merges four layers: managed → command line → project (.claude/settings.json) → user (~/.claude/settings.json). If a tool is denied at any higher layer, no lower layer can re-allow it. Lock things down at the user level once and forget it.
04
The Bash escape hatch
Where deny rules quietly fail
Read(./.env) does not block cat .env

This is the gotcha that bit me hardest. A Read deny rule only blocks the Read tool. Claude can still spawn a Bash shell and run cat .env, grep -r SECRET ., or find . -name "*.env" -exec cat {} \;. The Bash tool is a completely separate permission scope, and the contents of the file flow straight back into context like any other shell output.

The fix: deny Bash too

For every sensitive path, add matching Bash denies. It is verbose, but it closes the hole.

extra Bash denies
"deny": [
  "Read(./.env)",
  "Bash(cat .env)",
  "Bash(cat .env.*)",
  "Bash(grep:* .env*)",
  "Bash(less .env*)",
  "Bash(head .env*)",
  "Bash(tail .env*)"
]
The nuclear option: sandbox

If you do not want to play whack-a-mole, enable sandboxing. Claude Code can run inside a restricted environment (Claude Cowork on Mac, the dev container approach on Linux, or the new --sandbox flag) where the filesystem itself blocks reads of paths outside the project. This is the only approach that actually stops a determined agent.

05
A pre-tool-use hook for true parity
If you really want a .claudeignore file
The community workaround

Claude Code does support hooks: shell scripts that fire on lifecycle events. A PreToolUse hook runs before any tool call, can inspect the arguments, and can return an exit code that blocks the call. The community package li-zhixin/claude-ignore uses exactly this mechanism to give you a real .claudeignore file.

You install it, drop a .claudeignore at the root of your project with gitignore-style patterns, and the hook blocks any Read, Grep, or Glob call that matches. The patterns cascade hierarchically the way .gitignore does: drop one in a subdirectory and it applies only to that subtree.

.claudeignore (via hook)
# Build outputs
dist/
build/
.next/
out/

# Dependencies
node_modules/
vendor/
.venv/

# Test data
tests/fixtures/large/
*.snapshot

# Logs and caches
*.log
.cache/
coverage/

# Secrets (belt and braces — also denied in settings.json)
.env
.env.*
*.pem
*.key

Two caveats. First, the hook only intercepts the tools it knows about — if Claude finds a way around them, you fall through. Second, you are trusting a third-party hook script in your agent's privileged path. Read the source before you install it.

06
My actual setup
What I run on this blog repo
Three layers, in order of trust
1User-level deny in ~/.claude/settings.json for the universal stuff: ~/.ssh/**, **/.env*, **/*.pem, **/*.key, ~/.aws/**. These apply to every project I touch and I never have to think about them again.
2Project-level deny in .claude/settings.json committed alongside the code: node_modules/**, dist/**, .next/**, plus any project-specific paths like generated SDKs or large fixture dumps.
3Bash escape-hatch denies mirroring the Read rules for anything secret. I do not bother for build folders — wasting tokens on node_modules is annoying but not dangerous.
A real before/after

On my last refactor, before I added the project-level denies, a single "find all uses of renderPost" task burned 27k tokens, most of it grep'ing through node_modules and the build output. After adding the denies, the same task came back in 4k tokens and the answer was cleaner because Claude was not pattern-matching minified code by mistake.

Six lines of JSON, 80% token reduction. There is not a refactor or rename in your future where this does not pay for itself in the first hour.

Bottom line
Stop waiting for .claudeignore. Use what already works.

A native .claudeignore may ship one day. Until then, every Claude Code project should have a .claude/settings.json with deny rules for secrets and build folders, and a matching set of Bash denies for anything truly sensitive. Five minutes of setup, every token and every secret saved from here on out.