Skip to content

Build1 publisher3 min readPublished Updated

Claude Code's Read deny rules let grep -r and CLAUDE.md imports reach denied files

Claude Code 2.1.285's Read deny rules let 4 of 11 file-reading routes reach the model in a developer's test, among them grep -r and CLAUDE.md @imports. The docs limit the rules to named paths and point anyone needing a hard block to the OS sandbox.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Claude Code's Read deny rules let grep -r and CLAUDE.md imports reach denied files
Generated illustration

What happened

  • The Read, Grep and Glob tools held in the test, as did cat, head and grep pointed at a named file, and @ mentions in the prompt.
  • The permissions page states that deny rules do not apply to commands that read files without naming them or to scripts that open files themselves.
  • A route counted as held only if its canary marker was absent from both the model's final answer and the session transcript Claude Code writes to disk.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint A Read deny rule only stops access that names a matching path, so a secret inside a tree the agent can grep recursively or open from a script stays reachable.
  • exposure Any CLAUDE.md in the project can pull a denied file into context at launch with a relative @import, and the deny-rule docs do not cover that route.
  • contradiction The settings reference says deny entries exclude matching files from search results, yet default search on macOS, Linux and WSL goes through Bash grep, where the permissions page says the rule does not reach.
  • decision Teams that need a hard block on a secret are left with the sandbox, the only control the docs describe as stopping every process from reaching a path.

`Read(./secrets/**)` is a path rule. It fires when something names a path that matches it. The permissions page lists what it covers: Claude's built-in file tools, file commands Claude Code recognizes in Bash such as cat, head, tail, sed and tee, and the targets of Bash redirections [9]. `grep -r pattern .` names a directory. The denied file gets opened during the walk and never appears on the command line [10]. The same page says the rule does not apply to that command, or to "a Python or Node script that opens files itself" [10].

That exception would matter less if the model rarely used grep. One line in the tools reference makes grep the default: "On macOS, Linux, and WSL, Claude Code leaves Glob and Grep out of the default tool set, and Claude searches with find and grep through the Bash tool instead" [15]. The Grep tool held in the dev.to author's test [2]. On those three platforms the model does not start with it. Asked plainly for the token, the model ran grep -rn first and printed the marker in both runs [3].

The two import routes are the leak the documentation does not describe. Neither the permissions page nor the settings entry for permissions.deny mentions @path imports in CLAUDE.md [13]. The memory page says imported files are "expanded and loaded into context at launch alongside the CLAUDE.md that references them" [14]. Imports from a root CLAUDE.md and from one in a subdirectory both leaked, in 2 of 2 runs each [1]. The subdirectory file reached the secrets with `../` paths [17]. The content is in context from launch, so this route does not wait on any command the model picks [14].

The settings reference says a deny entry "excludes matching files from file discovery and search results, denies reads of them" [12]. A recursive grep through Bash is a search, and the marker came back from it [3]. The permissions page is more careful and calls its coverage of the built-in tools and @file mentions "a best-effort attempt" [8]. The recipe sits in the first sentence of the Read section and the exceptions sit in a warning box below it [7][10].

The test itself is good work. Each file held a unique canary string, a third file was left allowed as a positive control, and a route counted as held only if its marker was absent from both the final answer and the session transcript under `~/.claude/projects/` [5][6]. What the model said it saw never counted on its own [6].

The sample is small. It covers two runs per route across 20 sessions, one Claude Code version, and `--model sonnet` resolving to claude-sonnet-5-5 [1][4]. For the result to transfer to another repo, split it in two. The grep -r and Python routes are documented exclusions, so I'd expect them to carry over regardless of model [10]. Whether a model reaches for grep -rn first is model behavior, and a different model could pick differently [3][4].

In my view the deny list is a guard on Claude's own file tools and on the shell commands Claude Code can parse. For a secret, the permissions page gives its own answer: "For OS-level enforcement that blocks all processes from accessing a path, enable the sandbox" [11]. All 11 routes in this test exercised deny rules, so they do not show how the sandbox handles the four that leaked [1]. Moving `.env` out of the project tree takes it out of what grep -r walks from the project directory, but a script can still open it by path [10].

What to watch

  • Whether a later Claude Code release applies Read deny rules to CLAUDE.md @imports, or documents the gap on the permissions page.
  • A rerun of the same 11 routes with the sandbox enabled, the control the docs name for OS-level enforcement.
  • Whether Glob and Grep return to the default tool set on macOS, Linux and WSL, moving default search back onto a route that held.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories