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.
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.
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.
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.
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.
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.
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.
{
"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.
Read(**), a project-level Read(./.env) deny will still block it..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.
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.
For every sensitive path, add matching Bash denies. It is verbose, but it closes the hole.
"deny": [ "Read(./.env)", "Bash(cat .env)", "Bash(cat .env.*)", "Bash(grep:* .env*)", "Bash(less .env*)", "Bash(head .env*)", "Bash(tail .env*)" ]
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.
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.
# 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.
~/.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..claude/settings.json committed alongside the code: node_modules/**, dist/**, .next/**, plus any project-specific paths like generated SDKs or large fixture dumps.node_modules is annoying but not dangerous.
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.
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.