# Claude Code File System Boundaries in 2026: What `--add-dir`, Allowed Paths, and Sandbox Roots Actually Restrict

> Source: <https://dev.to/jsmanifest/claude-code-file-system-boundaries-in-2026-what-add-dir-allowed-paths-and-sandbox-roots-21mp>
> Published: 2026-10-24 06:07:19+00:00

`--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).
