Sergen Tanguc
Publication: OCT 2026 Reading time: 4 min

Stopping Claude Code From Reading Parent CLAUDE.md Files

#claude-code #tools

Claude Code starts in the working directory and walks up to /. Every CLAUDE.md and CLAUDE.local.md it meets on the way gets loaded into the session.

That’s fine for a single repo. It’s less fine when repos are nested: a monorepo, or a layout like ~/Projects/<client>/<repo>. The repo inherits instructions written for its parents, sometimes for a different client, and the model follows them.

The setting exists

claudeMdExcludes takes absolute paths or globs and skips the matching files. It works in user, project, local or managed settings, and the arrays from each level are combined. The one thing you can’t exclude is a managed policy CLAUDE.md.

So the question is which patterns to put in it.

Why one glob doesn’t do it

My first idea was "../**/*". That matches nothing. Patterns are compared against absolute paths, so a relative one never lines up.

An absolute <parent>/** matches too much: the repo sits under its parent, so its own CLAUDE.md gets excluded along with everything else.

There’s no documented negation syntax either, so “every parent except me” can’t be written as one rule. It has to be spelled out one parent directory at a time.

The useful trick is that a single * doesn’t cross /. CLAUDE*.md under a given folder matches CLAUDE.md and CLAUDE.local.md in that folder and nowhere below it.

The helper

I wrote a zsh function, claude-exclude-parents [-n] [-u] [dir], that lives in my dotfiles. Run from a repo, it:

  • finds the git root, or falls back to the current directory
  • walks every parent up to /
  • adds <p>/CLAUDE*.md, <p>/.claude/CLAUDE.md and <p>/.claude/rules/** for each parent <p>
  • merges the result into <repo>/.claude/settings.local.json

settings.local.json is machine-local, so teammates with a different directory layout aren’t affected.

The core is a loop and a jq merge:

Terminal window
local d=${root:h} p
while :; do
p=${d%/}
pats+=("$p/CLAUDE*.md" "$p/.claude/CLAUDE.md" "$p/.claude/rules/**")
[[ $d == / ]] && break
d=${d:h}
done
{ [[ -e $f ]] && cat $f || print '{}' } | jq --indent 2 '
.claudeMdExcludes = reduce $ARGS.positional[] as $p
(.claudeMdExcludes // [];
if any(.[]; . == $p) then . else . + [$p] end)
' --args "${pats[@]}" > $tmp && cat $tmp > $f

The parts that took longer than the loop

Writing a config file from a shell function is the easy part. Not damaging the file is where the rest of the code went.

Merge, don’t overwrite. jq keeps existing keys (permissions and so on) and their order, and only appends patterns that aren’t there yet. A second run changes nothing.

Refuse bad input. If the file isn’t valid JSON, or isn’t a JSON object, the function stops and leaves it alone.

Never write user-level config. If the resolved root is $HOME or /, it refuses to run.

cat tmp > file, not mv. mv replaces the inode, which drops the original permissions and turns a symlink into a regular file. Writing through cat keeps both.

Clean up on any exit. The temp file is removed in a zsh always block, so an error or Ctrl-C doesn’t leave it behind.

Keep the file out of git. If git doesn’t already ignore settings.local.json, the function appends it to .git/info/exclude. That file is local-only. The tracked .gitignore is never touched.

Keep the user file by default. ~/.claude/CLAUDE.md and ~/.claude/rules usually hold instructions you want everywhere, so they stay loaded. -u excludes them too. -n is a dry run that only prints the patterns.

I tested it against a throwaway home (HOME=/tmp/..., GIT_CONFIG_GLOBAL=/dev/null). The dry run wrote nothing, a rerun added no duplicates, permissions stayed at 600, existing permissions survived, and each guard fired when it should.

Caveats

Paths are resolved with zsh’s ${root:A}, which follows symlinks. On macOS /tmp becomes /private/tmp. If Claude Code compares against the unresolved path, the patterns won’t match. Check with /context in a fresh session: the memory files listed should be the repo’s own and nothing above it.

Moving a repo leaves the old patterns in place. They’re harmless, but rerun the function after a move.

This only covers parents. CLAUDE.md files in subdirectories below the working directory still load on demand when Claude reads files there.