.claudeignore cookbookWhat works, what breaks, what to layer on top.
You wrote a .claudeignore. You tested a few paths. Everything looks right. The agent still read your .env file on Tuesday. Here is why that happens and how to close the gaps.
-
01
Source
.claudeignore written
patterns added, assumed to filter everything
1 file · project root -
02
Mechanism
Partial enforcement
context reads blocked, workspace search bypasses
tool-call dependent -
03
Result
Secrets in context
env vars reach the agent anyway
silent, no warning
TL;DR· the answer, in twenty seconds
What: .claudeignore controls what Claude Code reads into context through file-read tool calls. It does not filter workspace search results, inline tool outputs, or files the user pastes directly. The Register reported in January 2026 that directory-anchored patterns and patterns overlapping with workspace search silently fail.
Fix: layer .claudeignore with settings.json read-only path restrictions (permissions.deny) and move secrets out of the repo tree entirely. Use hasp check-repo to audit what the agent can reach before the next session.
Lesson: ignore files control one path into context. Secrets need to stay out of the directories the agent writes to and searches, not just out of the files it directly reads.
.claudeignore looks like .gitignore. It lives in the project root. The syntax is the same. That similarity is a trap.
Git's ignore rules stop at one job: telling git which files to pretend do not exist. .claudeignore does something narrower. It tells Claude Code not to read matched files into its context window through direct file reads. That distinction collapses under about six common conditions, and The Register broke the specifics in January 2026: directory-anchored patterns fail silently, and patterns that overlap with the workspace search index let content through anyway.
What follows is a pattern-by-pattern cookbook. Each recipe shows the glob, what it reliably catches, where it breaks, and the settings.json line that closes the gap.
What to know in 60 seconds
.claudeignoreblocks file reads into the context window. It does not filter workspace search results.- Patterns anchored with a leading slash (e.g.,
/.env) match only the project root on most Claude Code builds. - Double-star globs (
**/secrets/**) work for file reads but not for thegrep-style search tool calls the agent uses during exploration. settings.jsonpermissions.denyblocks the read-file tool call entirely for matched paths, regardless of how the agent found the file.- Nothing in
.claudeignorestops a user from pasting the content of a secret file directly into the chat.
How .claudeignore actually works
Claude Code processes .claudeignore before certain tool calls that read file content. When the agent decides to read /your/project/.env, the tool checks the path against the ignore list and refuses if it matches. That is the entire mechanism.
Two categories of access fall outside it.
The first is workspace search. Claude Code indexes your project for semantic and keyword search, which it uses to answer questions like "where is the database connection string defined?" The search returns file paths and snippets. .claudeignore does not filter those snippets. If your .env contains DATABASE_URL=postgres://user:password@host/db, a workspace search for "database connection" may surface that line. The agent reads the result; no file-read tool call fires; the ignore rule never triggers.
The second is anything the user puts into the prompt manually. A developer who pastes the output of cat .env has bypassed every file-level control. This is not a flaw. It is user intent. But it matters for threat models that assume the agent is constrained by its ignore rules.
The Register's January 2026 reporting named two additional failure modes: directory-anchored patterns that silently fall through, and a race between pattern resolution and the agent's startup read of its own context files. Both are covered below in the relevant recipes.
15 concrete recipes
1. .env files
.env
.env.*
.env.local
.env.*.local
Catches: direct reads of .env, .env.production, .env.local, and similar variants in the project root.
Misses: .env files in subdirectories unless you add **/.env separately. Workspace search that surfaces env var values as snippets from any indexed file. Files named env without the dot.
settings.json fallback:
{
"permissions": {
"deny": ["Read(.env*)", "Read(**/.env*)"]
}
}
2. AWS credential files
.aws/credentials
.aws/config
**/.aws/credentials
**/.aws/config
Catches: the standard AWS CLI credential paths when read directly.
Misses: AWS_ACCESS_KEY_ID exported in your shell and captured in .claude/settings.local.json (the Knostic February 2026 disclosure). Any file that contains AWS keys but is not named credentials or config.
settings.json fallback:
{
"permissions": {
"deny": ["Read(.aws/**)", "Read(**/.aws/**)"]
}
}
3. PEM and private key files
**/*.pem
**/*.key
**/*.p12
**/*.pfx
**/*.jks
id_rsa
id_ed25519
*.pub
Catches: direct reads of certificate and key files by extension.
Misses: keys stored with unusual extensions or no extension (a common practice in container secrets mounts at /run/secrets/tls_key). Keys copied inline into YAML config files.
Note on *.pub: listing public key files is optional but stops the agent from reading your key inventory and deducing the private key filenames.
settings.json fallback:
{
"permissions": {
"deny": ["Read(**/*.pem)", "Read(**/*.key)", "Read(**/*.p12)"]
}
}
4. Generic secrets directories
**/secrets/**
**/secret/**
**/.secrets/**
**/private/**
Catches: file reads inside directories named secrets, secret, or private at any depth.
Misses: The Register's January 2026 report specifically named this pattern as one that bypasses workspace search. If the agent uses a search tool call to find files inside secrets/, it gets back paths and snippets before any read-file check fires.
settings.json fallback:
{
"permissions": {
"deny": ["Read(**/secrets/**)", "Read(**/private/**)"]
}
}
5. SSH config
.ssh/config
**/.ssh/**
Catches: direct reads of your SSH config and key directory.
Misses: ~/.ssh/ is outside the project tree. .claudeignore only covers paths relative to the project root. Your SSH config is not at risk from .claudeignore gaps; it is at risk from the agent being given a home directory as its working directory.
settings.json fallback: not applicable for paths outside the project tree. The relevant control is not running Claude Code with $HOME as the working directory.
6. Docker and container secrets
docker-compose.override.yml
docker-compose.*.yml
**/.docker/config.json
Catches: override compose files that typically hold real credentials, and Docker daemon auth config.
Misses: docker-compose.yml itself (intentionally omitted, since you probably want the agent to read it). Inline environment: blocks inside compose files you do want the agent to access. Secrets passed via --env-file flags in run scripts.
settings.json fallback:
{
"permissions": {
"deny": ["Read(docker-compose.override.yml)", "Read(**/.docker/config.json)"]
}
}
7. Terraform state and vars
*.tfstate
*.tfstate.*
*.tfvars
**/.terraform/**
Catches: Terraform state files (which contain output values including secrets), variable files with real values, and the local .terraform/ cache directory.
Misses: terraform.tfvars.json unless you add *.tfvars.json. Output values embedded in plan files. State stored in remote backends (S3, Terraform Cloud) is not on disk, so .claudeignore is irrelevant there.
settings.json fallback:
{
"permissions": {
"deny": ["Read(*.tfstate)", "Read(*.tfvars)", "Read(**/.terraform/**)"]
}
}
8. Kubernetes secrets manifests
**/secrets.yaml
**/secrets.yml
**/*-secret.yaml
**/*-secret.yml
**/*secret*.yaml
Catches: YAML files that follow the common naming convention for Kubernetes Secret manifests.
Misses: secrets embedded in non-secret-named manifests. SealedSecret resources (these are encrypted, but leaking them gives attackers the ciphertext). ConfigMaps that contain base64-encoded credentials named to look like ordinary config.
settings.json fallback:
{
"permissions": {
"deny": ["Read(**/*secret*.yaml)", "Read(**/*secret*.yml)"]
}
}
9. Node.js toolchain internals
node_modules/.bin/
node_modules/.cache/
.npm/
.npx/
Catches: binary symlinks and npm caches that the agent should not be exploring.
Misses: .claudeignore trailing slashes for directory matches behave inconsistently across Claude Code versions. The safe form is node_modules/.bin/** and node_modules/.cache/** with an explicit glob.
settings.json fallback:
{
"permissions": {
"deny": ["Read(node_modules/**)", "Read(.npm/**)"]
}
}
10. IDE and editor temp files
.idea/
.vscode/settings.json
.vscode/launch.json
*.swp
*.swo
.DS_Store
Catches: JetBrains project files (which can include database passwords stored in run configurations), VS Code settings with API endpoint credentials, and editor temporaries.
Misses: .vscode/extensions.json and .vscode/tasks.json are not covered here. Some teams store environment-specific launch configs with embedded tokens. Cover the whole directory if you are not sharing VS Code configs in version control.
settings.json fallback:
{
"permissions": {
"deny": ["Read(.idea/**)", "Read(.vscode/**)"]
}
}
11. Python virtual environments
.venv/
venv/
env/
.env/
__pycache__/
*.pyc
Catches: local Python environments, which can contain pyvenv.cfg files that reference Python paths and pip configurations with index server credentials.
Misses: pip.conf and ~/.config/pip/pip.conf are outside the project tree. A virtualenv activate script that sources environment variables from outside the venv.
settings.json fallback:
{
"permissions": {
"deny": ["Read(.venv/**)", "Read(venv/**)", "Read(__pycache__/**)"]
}
}
12. Go build artifacts and vendor
vendor/
dist/
build/
bin/
Catches: vendored dependencies, built binaries, and distribution artifacts.
Misses: vendor/ sometimes contains a modules.txt that lists module paths. Some teams embed credentials in build scripts inside dist/. Built Go binaries are unreadable as text, but the agent will attempt to read them, which wastes context.
settings.json fallback:
{
"permissions": {
"deny": ["Read(vendor/**)", "Read(dist/**)", "Read(bin/**)"]
}
}
13. CI/CD pipeline credentials
.github/workflows/*.yml
.circleci/config.yml
.gitlab-ci.yml
Note: you probably want the agent to read these. The risk is that developers occasionally hardcode secrets in pipeline YAML instead of using secret variables. Ignoring the whole directory is usually wrong. Instead, run git grep -i 'password\|secret\|token\|key' .github/workflows/ before each session.
Alternative approach: scan with hasp check-repo to identify hardcoded credentials in CI config before giving the agent access.
14. Local development overrides
*.local
*.override
config.local.*
database.yml
Catches: local override files that follow the pattern of containing real credentials for development environments. Rails database.yml files frequently have production connection strings that were never removed.
Misses: application.local.properties (common in Spring Boot projects) unless you add it explicitly. Anything named .local at a path deeper than one directory unless you add **/*.local.
settings.json fallback:
{
"permissions": {
"deny": ["Read(*.local)", "Read(**/*.local)", "Read(database.yml)"]
}
}
15. Agent state files from other tools
.cursor/
.aider/
.codex/
.copilot/
Catches: state files from Cursor, Aider, and Codex that may hold session context, captured environment variables, or conversation history containing secrets.
Misses: Knostic disclosed the Cursor .cursor/ leak in late January 2026. If you have multiple coding agents in the same repo, each has its own state directory. Cover them all.
settings.json fallback:
{
"permissions": {
"deny": ["Read(.cursor/**)", "Read(.aider/**)", "Read(.codex/**)"]
}
}
Where the rules actually break
The Register's January 2026 reporting identified two failure patterns worth dwelling on.
Directory-anchored patterns. A pattern like /secrets/ with a leading slash is supposed to anchor to the project root. In Claude Code, the behavior depends on which internal path resolver handles the check. During startup, when the agent reads its own context files, the resolver runs before the ignore list fully loads. Files read in that window bypass all ignore rules. This is the race condition The Register documented.
Workspace search overlap. The agent's default exploration mode uses a search tool that returns file paths and content snippets. This tool call does not go through the same file-read path that .claudeignore intercepts. A pattern like **/secrets/** blocks the Read() tool for files inside secrets/. It does not block a Grep() or SearchFiles() call that happens to return content from the same directory. The agent gets the content; no rule fires; no warning appears.
The practical consequence: .claudeignore is reliable for large, obvious files you want to exclude from the agent's reading list. It is unreliable as a security boundary for secrets that the agent might discover through search.
What .claudeignore is not
.claudeignore is not .gitignore. Git uses the ignore file to decide what to track. Claude Code uses .claudeignore to decide what to read into context. The two files are independent. A file listed in .claudeignore can still be committed to git and pushed to a public repository. A file ignored by git can still be read into Claude's context unless you also list it in .claudeignore.
.claudeignore is not a permissions.deny rule. settings.json permissions.deny blocks the tool call at the API level. The agent cannot call Read() on a denied path, period. .claudeignore works one layer up: the file read is technically attempted, the path is checked against the pattern, and the result is suppressed. The deny rule is more reliable for high-value paths.
.claudeignore is not a substitute for keeping secrets out of the repo. If .env.production is committed to git and present on disk, your ignore file stops Claude Code from reading it directly. Workspace search, another developer, a CI script, and git log -p can all still reach it. The root fix is to never commit secrets, not to ignore them after the fact.
A pasteable checklist for your PR
## .claudeignore audit
- [ ] .env and .env.* in both .claudeignore and .gitignore
- [ ] .aws/credentials and **/.aws/** in .claudeignore
- [ ] **/*.pem, **/*.key, **/*.p12 in .claudeignore
- [ ] **/secrets/** and **/private/** in .claudeignore
- [ ] docker-compose.override.yml and **/.docker/config.json in .claudeignore
- [ ] *.tfstate and *.tfvars in .claudeignore
- [ ] **/*secret*.yaml and **/*secret*.yml in .claudeignore
- [ ] node_modules/.bin/**, node_modules/.cache/** in .claudeignore
- [ ] .idea/**, .vscode/settings.json, .vscode/launch.json in .claudeignore
- [ ] .venv/**, venv/**, __pycache__/** in .claudeignore
- [ ] .cursor/**, .aider/**, .codex/** in .claudeignore
- [ ] settings.json permissions.deny set for high-value paths
- [ ] git grep -i 'password\|token\|secret\|key' .github/workflows/ clean
- [ ] hasp check-repo run and no unredacted secrets found
- [ ] No .env files committed in git log --all
What this means for your stack
.claudeignore does one thing well: it keeps large, predictable files out of the agent's reading list. For secrets that travel through workspace search, inline tool calls, or environment variables the agent inherits at startup, the file does nothing. The layered answer is .claudeignore for broad file exclusions, settings.json permissions.deny for high-value paths, and secrets removed from the repo tree entirely before the agent session starts.
hasp is one working implementation of the last part. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, and the agent gets a reference instead of a live credential. hasp check-repo scans the project tree before each session and reports unredacted secrets the agent would otherwise find. Source-available (FCL-1.0), local-first, macOS and Linux, no account.
The durable rule: secrets do not belong in directories the agent can search. Ignore files manage noise. They are not a perimeter.
Sources· cited above, in one place
Stop handing the agent your real keys.
hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.
- Local, encrypted vault — no account, no cloud, no telemetry by default.
- Brokered run — agent gets a reference, the child process gets the value.
- Pre-commit + pre-push hooks catch managed values before they ship.
- Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.