Skip to content

Grant the workload files and directories

A box starts with almost nothing on the filesystem. You widen it in two places, and which one you need depends on how the workload touches the file:

RouteExampleWhat decides
The agent’s own system callsThe harness’s file-edit tool writes src/main.rs[agent.filesystem] in box.toml
A tool’s own system callsgit reads .git/config[tool.<name>.filesystem] in box.toml
A command in the box’s shell or Pythoncat README.md, open("data.csv")fs:* rules in the policy

A box.toml grant is one of the sandbox grants: the operating system enforces it, and no policy decision or record covers it. A shell or Python file operation is a policy decision, recorded like every other. A harness with its own file tools, such as Claude Code or Codex CLI, can use both: a box for it grants the project in box.toml and permits it in policy.dw. To have the policy decide every file, grant the project only in list. The harness’s file tools then fail, and it falls back to its shell, as Run Claude Code and Run Codex CLI show.

Add lists to [agent.filesystem]:

[agent.filesystem]
read = ["~/src/my-service", "~/.local/share/claude/versions/2.1.286"]
write = ["~/src/my-service"]
exec = ["~/.local/share/claude/versions/2.1.286"]
deny = ["~/src/my-service/secrets"]

The agent table takes eight lists:

ListA directory entry grantsA file entry grants
readRead of everything below itRead of that file
writeWrite of everything below itWrite of that file
read_fileRefused: use readRead of that file
write_fileRefusedWrite of that existing file
listDirectory listings and stat, no contentRefused
metadatastat onlyRefused
execRunning every binary below itRunning that executable
denySubtracts the tree from every other grantSubtracts the file

write doesn’t include read on macOS. List a path in both read and write when the agent edits files there.

Each [tool.<name>] takes its own filesystem table with six of the lists: read, write, read_file, write_file, list, and deny. A tool can’t take metadata, because it can test for and read the metadata of paths across your home by default, or exec, because on macOS it can run any binary it can reach, and load code it writes into its writable grants. A stdio MCP server’s [mcp.<name>.filesystem] takes the same six lists, and box run refuses metadata and exec in either table by name.

[tool.git.filesystem]
read = ["~/src/my-service", "/Library/Developer/CommandLineTools"]
write = ["~/src/my-service"]

On macOS, a tool whose program runs an Apple /usr/bin stub, such as /usr/bin/git, /usr/bin/python3, cc, or make, directly or through a child process, needs the active developer directory in its read list, as above. xcode-select -p prints the directory.

A tool’s grants are its own. Granting a path to git doesn’t grant it to the agent, and the reverse holds too. Add a tool covers the rest of a tool table.

Permit interpreter file access in the policy

Section titled “Permit interpreter file access in the policy”

The box’s shell and Python raise an fs:read, fs:write, fs:delete, or fs:move decision for each file operation, on the resolved path. Permit a tree with a path condition:

@id("project_read")
permit (principal, action == Box::Action::"fs:read", resource)
when {
context.input.path == "~/src/my-service" ||
context.input.path like "~/src/my-service/*"
};
@id("project_write")
permit (principal, action == Box::Action::"fs:write", resource)
when { context.input.path like "~/src/my-service/*" };

Write a path under your home as ~/…, and a directory without a trailing slash. Box refuses to load a rule that spells either one another way, because it could never match.

Always give an fs:read permit a path condition. Box refuses no credential path for the interpreters on its own, so permit fs:read with no when lets a shell command read ~/.aws and ~/.ssh. To protect a file’s content, forbid fs:delete on it as well as fs:write: a rename that overwrites the file counts as a delete of the destination.

A path the shell doesn’t bind to a real directory lives in the shell’s memory for one command, and is gone when the command ends. The shell writes at most 10 MiB to such a file. The write that crosses the limit fails with file size limit exceeded (10485760 bytes). A file in the project, or in another bound directory, has no limit from the shell.

The policy action reference lists every fs:* operation. For the Python side, see Python in a box.

Box checks every box.toml filesystem entry before the workload starts, and refuses the run if one fails:

  • Spelling. Absolute, or starting with ~/. No .., no glob characters (*, ?, [, ], {, }), no control characters, and no duplicates. A directory entry already covers everything below it.
  • The path exists and isn’t a symbolic link. On macOS, a deny entry can name a path that doesn’t exist yet.
  • The spelling is canonical, the path the kernel checks. On macOS write /private/tmp/work, not /tmp/work. The refusal names the canonical spelling.
  • No nesting. Two grants of the same operation can’t overlap, and a deny can’t cover a grant entirely.
  • Not the box’s own directory. An entry can’t reach into box_dir, and, except in metadata, can’t enclose it. read = ["~/boxes"] is refused when box_dir is /Users/me/boxes/x.
  • No write on PATH when MCP servers are declared. In a box with a stdio MCP server, a write grant at or above a directory on your PATH is refused.

Box also refuses grants on these paths:

  • The roots of system and home trees: /, /Users, /home, /System, /Library, /usr, /etc, /private, /tmp, /var, and similar. A path inside one, such as /Library/Developer/CommandLineTools, can be granted.
  • Machine secrets: password databases, system keychains, sudoers, and the TCC database, and the paths inside them.
  • Trees that enclose a credential store: a grant such as read = ["~"] that contains ~/.aws, ~/.ssh, ~/.gnupg, ~/.netrc, ~/.docker, ~/.kube, ~/.config/gcloud, ~/.mozilla, ~/Library/Keychains, or a Chrome or Firefox profile directory.

A read, write, read_file, or write_file entry that names a credential store or a path inside one is allowed, and Box announces it at startup:

strands-box: [agent]: exposes ~/.aws/config (read_file)

Box prints the agent’s and each tool’s grants on stderr when the box starts, before the agent runs. It doesn’t print deny entries, or the grants of MCP servers:

strands-box: [agent] runs /Users/me/bin/claude with no policy decision over these paths:
read /Users/me/src/my-service
write /Users/me/src/my-service
exec /Users/me/bin/claude (command, implicit)
strands-box: [agent] runtime minimum, added by Core:
...
enter /Users/me/src/my-service (workspace, entry only)
strands-box: [agent] HOME=/Users/me PATH=...

The runtime minimum is the small set of system paths a process needs to start, such as /System/Library and /dev/null on macOS. Box adds it to every box and lists it here.

The agent’s workspace is its starting directory. The agent can change into it, but can’t read or write it until a list grants it.