Claude Code File System Boundaries in 2026: What `--add-dir`, Allowed Paths, and Sandbox Roots Actually Restrict A developer's technical analysis shows that Claude Code's `--add-dir` flag and `additionalDirectories` setting expand the Bash sandbox root set, while `allowRead`, `allowEdit`, `denyRead` and `denyEdit` separately govern which files Claude's read/write tools can access, and that configuring one layer does not constrain the other. The writeup warns that teams copying `--add-dir` flags from documentation can end up with agents reading `.env` files or modifying sibling directories, and that re-enabling project access with `allowRead: ['.']` re-exposes every file under the working directory unless explicitly denied. --add-dir , Allowed Paths, and Sandbox Roots Actually Restrict This article was written with the assistance of AI, under human supervision and review. Teams enable Claude Code in production repositories with --add-dir flags they copied from documentation, then discover the agent reading .env files or modifying sibling project directories during auto mode. The failure stems from conflating two independent permission layers that operate at different abstraction levels. Problem flow showing Claude Code reading secrets due to misconfigured permissions The correct model separates sandbox roots which directories the Bash tool can execute commands inside from allowed paths which files Claude's read/write tools can access directly . Configuring one layer does not constrain the other. Solution flow showing properly isolated permissions This post dissects the boundary mechanics teams must internalize before granting auto-approve permissions. The difference between a contained agent and one that leaks credentials sits in understanding these two layers. additionalDirectories determine where Bash commands execute, while allowed paths controlled by allowRead , allowEdit , denyRead , denyEdit govern which files Claude's tools can access directly. allowRead: '.' to re-enable project access also re-exposes every file under the working directory, including .env and secrets/ , unless explicitly denied in the same configuration block. cat .env command succeeds even when The --add-dir flag controls sandbox root expansion. When a developer starts Claude Code with claude-code --add-dir ../shared-utils , the Bash sandbox gains write access to that directory. Commands like cd ../shared-utils && npm install now succeed because the sandbox boundary extends to include that path. This permission operates at the process execution layer. The Bash tool can create, modify, and delete files anywhere inside a sandbox root. No allowRead or allowEdit configuration constrains what happens inside a bash tool invocation. Diagram showing sandbox root expansion with --add-dir flag The additionalDirectories field in a permission configuration file .claude/permissions.jsonc provides the same capability programmatically. Both mechanisms add paths to the sandbox root set, which starts with the working directory and a per-user temp directory by default. The trap appears when teams assume that adding a directory to the sandbox automatically grants Claude's read file or write file tools permission to access those files. It does not. A --add-dir ../secrets flag allows Bash commands to manipulate that directory, but Claude cannot directly read or write files there unless the allowed paths configuration explicitly permits it. Conversely, granting allowRead: '../secrets' without adding that directory as a sandbox root means Claude can read files directly, but any Bash command that attempts to cd ../secrets fails because the sandbox boundary blocks it. The layers operate independently. Sandbox roots define where processes can execute. Allowed paths define which files Claude's tools can touch. The confusion costs teams hours when an agent behavior seems inconsistent, reading some files but not others, or executing commands in unexpected directories. Two-layer permission model showing sandbox roots and allowed paths A concrete scenario clarifies the split. A repository contains three directories: src/ , tests/ , and secrets/ . The developer starts Claude Code with --add-dir ./tests and configures: { "allowRead": "./src" , "allowEdit": "./src" , "denyRead": "./secrets" } Claude can directly read and write files under src/ using its read/write tools. When asked to run the test suite, the agent executes bash tool with cd tests && npm test , which succeeds because tests/ sits inside a sandbox root. If the agent tries to read tests/utils.test.ts using read file , the operation fails because tests/ does not appear in allowRead . The secrets/ directory sits outside both layers. Claude cannot read it directly denied , and no Bash command can access it not a sandbox root . This configuration achieves the intended isolation. Claude Code permission layers diagram showing sandbox and allowed path boundaries The failure mode emerges when teams add allowRead: '.' to fix a perceived overly restrictive setup. That single line re-exposes every file under the working directory to Claude's read tool, including secrets/ , unless an explicit denyRead rule blocks it. The sandbox root layer remains unchanged, but the allowed paths layer just opened wide. A test script proves the layer separation. The setup creates a directory structure with files at different permission levels, then attempts operations through both Bash and direct file tools. js // verify-boundaries.ts import { execSync } from 'child process'; import { writeFileSync, readFileSync, mkdirSync } from 'fs'; import { join } from 'path'; // Setup test structure const testRoot = './boundary-test'; mkdirSync join testRoot, 'allowed' , { recursive: true } ; mkdirSync join testRoot, 'sandbox-only' , { recursive: true } ; mkdirSync join testRoot, 'blocked' , { recursive: true } ; writeFileSync join testRoot, 'allowed', 'public.txt' , 'safe content' ; writeFileSync join testRoot, 'sandbox-only', 'build.log' , 'build output' ; writeFileSync join testRoot, 'blocked', 'secret.key' , 'credential' ; // Test 1: Direct read tool on allowed path try { const content = readFileSync join testRoot, 'allowed', 'public.txt' , 'utf-8' ; console.log '✓ read file on allowed path:', content ; } catch err { console.log '✗ read file blocked:', err.message ; } // Test 2: Direct read tool on sandbox-only path not in allowRead try { const content = readFileSync join testRoot, 'sandbox-only', 'build.log' , 'utf-8' ; console.log '✓ read file on sandbox-only:', content ; } catch err { console.log '✗ read file blocked on sandbox-only:', err.message ; } // Test 3: Bash command on sandbox-only path try { const output = execSync cat ${join testRoot, 'sandbox-only', 'build.log' } , { encoding: 'utf-8', cwd: testRoot } ; console.log '✓ bash tool on sandbox-only:', output ; } catch err { console.log '✗ bash tool blocked:', err.message ; } // Test 4: Bash command on blocked path outside sandbox try { const output = execSync cat ${join testRoot, 'blocked', 'secret.key' } , { encoding: 'utf-8', cwd: testRoot } ; console.log '✓ bash tool on blocked path:', output ; } catch err { console.log '✗ bash tool blocked on denied path' ; } Running this script with the configuration from the previous section demonstrates the boundary enforcement. Test 1 succeeds because the file sits in an allowed path. Test 2 fails because sandbox-only/ does not appear in allowRead , even though it sits inside a sandbox root. Test 3 succeeds because Bash can execute inside sandbox roots regardless of allowed paths. Test 4 fails because blocked/ sits outside the sandbox boundary entirely. The output proves that adding a directory to sandbox roots does not grant file tool access, and vice versa. Teams that skip this verification step deploy agents with permissions they did not intend to grant. The pattern allowRead: '.' appears in permission files when developers encounter repeated "permission denied" errors during development. The wildcard grants read access to every file under the working directory, which includes configuration files, environment variables, and any secrets stored in the repository. A repository with this structure demonstrates the risk: project/ ├── src/ ├── tests/ ├── .env ├── secrets/ │ └── api-keys.json └── .claude/ └── permissions.jsonc The following configuration seems safe because it denies the secrets/ directory: { "allowRead": "." , "denyRead": "secrets/" } Claude cannot read secrets/api-keys.json because the deny rule takes precedence. But .env sits directly in the working directory, fully exposed. The agent can read database credentials, API tokens, and any other environment secrets without restriction. Permission flow showing how allowRead re-exposes secrets The fix requires enumerating every secret path and denying it explicitly: { "allowRead": "." , "denyRead": ".env", ".env. ", "secrets/", " .key", " .pem", " .p12" } This approach scales poorly. Every new secret file type requires a configuration update. A safer pattern allows only the directories that Claude legitimately needs to read: { "allowRead": "src/", "tests/", "package.json", "tsconfig.json" , "allowEdit": "src/" , "denyRead": " " , // explicit default-deny "denyEdit": " " } The explicit default-deny rule makes the permission model fail-safe. Adding a new directory to the repository does not automatically grant access. Teams must consciously expand the allowed set, forcing a review of what the agent can reach. The permission model enforces different rules depending on whether Claude invokes a Bash command or calls a direct file tool. Understanding which layer blocks which operation prevents the "it worked in the shell but failed in the agent" confusion. Comparison of Bash tool and file tool permission enforcement | Operation | Layer Checked | Fails When | Succeeds When | |---|---|---|---| | bash tool: cat file.txt | Sandbox roots | File outside sandbox boundary | File in any sandbox root | | read file: file.txt | Allowed paths | File not in allowRead or in denyRead | File in allowRead and not denied | | bash tool: cd ../other && ls | Sandbox roots | ../other outside sandbox | ../other added via --add-dir | | write file: file.txt | Allowed paths | File not in allowEdit or in denyEdit | File in allowEdit and not denied | A practical scenario illustrates the split. A monorepo contains apps/web and packages/shared . The developer starts Claude Code from apps/web with: claude-code --add-dir ../../packages/shared The configuration file specifies: { "allowRead": "./src" , "allowEdit": "./src" } When Claude needs to build the app, it executes: bash tool: cd ../../packages/shared && npm run build This command succeeds because packages/shared sits in a sandbox root. The build writes output to packages/shared/dist , which also succeeds because Bash operates inside the sandbox boundary. If Claude then tries to read the build output directly: read file: ../../packages/shared/dist/index.js The operation fails. The file sits outside the allowed read paths. The sandbox layer permitted the build, but the allowed paths layer blocks the read. The agent must use bash tool: cat ../../packages/shared/dist/index.js to access the content, which routes through the sandbox layer instead. Diagram showing different permission enforcement for Bash versus direct file tools Teams encounter this pattern when CI scripts rely on build artifacts that the agent cannot read directly. The solution either adds the artifact directory to allowRead or restructures the workflow to keep everything inside allowed paths, eliminating the sandbox-only zone. Production deployments require a defense-in-depth model that assumes the agent will attempt operations outside its intended scope. The configuration must fail safely when Claude requests access to an unexpected path. Production permission lockdown flow The baseline configuration for a Next.js application running automated testing looks like: { "additionalDirectories": , // only working directory "allowRead": "app/", "components/", "lib/", "public/", "package.json", "next.config.js", "tsconfig.json", "playwright.config.ts" , "allowEdit": "app/", "components/", "lib/" , "denyRead": ".env ", "secrets/", " .key", " .pem", "node modules/", ".next/", "coverage/" , "denyEdit": "package.json", " .config. ", ".github/", "README.md" } This setup grants the agent read access to source directories and configuration files it needs to understand the codebase. Write permissions limit to source code only. Build artifacts, dependencies, and configuration files sit in deny lists. The additionalDirectories field remains empty, constraining Bash commands to the working directory. If the agent needs to run tests that write temporary files, those writes succeed because the working directory sits in the default sandbox root set. External directories remain unreachable. The explicit deny rules prevent accidental exposure if a developer later adds allowRead: '.' during debugging. The deny list takes precedence, maintaining the security boundary even when the allow list expands. A stricter variant for CI environments removes all write permissions: { "additionalDirectories": , "allowRead": "app/", "components/", "lib/", "package.json", "playwright.config.ts" , "allowEdit": , // no direct file writes "denyRead": " " , "denyEdit": " " } The agent can read source code and configuration but cannot modify files directly. Test runs that need to write output must use bash tool commands, which remain constrained to the working directory sandbox. This configuration supports agents that analyze codebases and run tests but cannot make changes autonomously. Teams deploying auto-approve modes must start from this restrictive baseline and expand permissions only when a specific, verified use case demands it. The expansion process requires documenting why each path needs access and what risk it introduces. For more details on broader permission strategies, see Claude Code Permission Modes, allowedTools, and Sandbox Configuration https://jsmanifest.com/claude-code-permission-modes-allowedtools-sandbox . No. The --add-dir flag expands the sandbox root set, which controls where Bash commands can execute. Claude's read file and write file tools remain governed by the allowed paths configuration. A file can sit inside a sandbox root but still be inaccessible to direct file tools if it does not appear in allowRead . Yes. The allowed paths layer operates independently from sandbox roots. If allowRead: '.env' appears in the configuration, Claude can read that file directly even if it sits outside every sandbox root. Bash commands attempting to cat .env would fail if .env sits outside the sandbox, but the read file tool succeeds because it checks a different layer. Deny rules take precedence. If a file matches both allowRead: '.' and denyRead: '.env' , the deny rule blocks access. This precedence enables safe use of broad allow patterns with specific exceptions for sensitive files. npm install work but read file: node modules/package/index.js fail? The npm install command runs through bash tool , which operates inside sandbox roots. The working directory sits in the default sandbox root set, so the command succeeds. Reading a file under node modules/ requires an explicit allowRead entry for that path. The sandbox layer permitted the install, but the allowed paths layer blocks the read. No. Production configurations should enumerate allowed paths explicitly and use denyRead: ' ' as a default-deny rule. The allow-with-exceptions pattern scales poorly and introduces risk when new files appear in the repository. Explicit allow lists force conscious permission expansion and make the security model auditable. Sandbox roots control where processes execute. Allowed paths control which files tools can touch directly. Conflating these layers leads to agents that leak secrets or fail mysteriously when permissions seem correctly configured. Production deployments require explicit allow lists, default-deny rules, and documentation of why each path needs access. The boundary between contained and compromised sits in understanding that --add-dir and allowRead operate on different mechanisms entirely. Teams that treat filesystem permissions as a single unified model will spend hours debugging behavior that the two-layer architecture explains immediately. For guidance on structuring agent workflows within these boundaries, see Claude Code CI: Agentic Review, Test, and Auto-Fix Workflows https://jsmanifest.com/claude-code-ci-agentic-review-test-auto-fix and Claude Code Cost Control in Production: Rate Limits, Budgets, and Safe Auto-Approve https://jsmanifest.com/claude-code-cost-control-production .