Recipe: Scoped file access
The problem
The agent needs to read source code and scratch files. It never needs
to read /etc/passwd, SSH private keys, or AWS credentials. A
vulnerability in the model (or a prompt injection) could otherwise
trick the agent into exfiltrating secrets from the filesystem.
The policy
version: '1'
# Scoped file access.
#
# Allow file_read and file_write generally, but explicitly deny known
# dangerous paths. Ordering matters: deny rules evaluate first and win.
#
# The extractor emits `file_read:read` and `file_write:write` for
# every Read/Write/Edit tool call regardless of path. To enforce
# per-path scoping, rule evaluation uses the `path` arg via the
# `when:` condition block.
settings:
default_action: deny
default_on_missing: deny
default_on_tamper: deny
rules:
- id: deny-etc
deny: 'file_read:read'
when:
path: '/etc/*'
reason: 'System config paths are out of scope.'
- id: deny-etc-write
deny: 'file_write:write'
when:
path: '/etc/*'
reason: 'Writes to /etc are never allowed.'
- id: deny-root-home
deny: 'file_read:read'
when:
path: '/root/*'
reason: 'Root home is out of scope.'
- id: deny-ssh
deny: 'file_read:read'
when:
path: '*/.ssh/*'
reason: 'SSH keys are never read by the agent.'
- id: deny-aws-creds
deny: 'file_read:read'
when:
path: '*/.aws/*'
reason: 'AWS credentials are never read by the agent.'
- id: allow-tmp-read
allow: 'file_read:read'
when:
path: '/tmp/*'
reason: 'Scratch space is readable.'
- id: allow-tmp-write
allow: 'file_write:write'
when:
path: '/tmp/*'
reason: 'Scratch space is writable.'
- id: allow-repo-read
allow: 'file_read:read'
when:
path: '/workspace/*'
reason: 'Repo tree is readable.'
- id: allow-repo-write
allow: 'file_write:write'
when:
path: '/workspace/*'
reason: 'Repo tree is writable.'
Why it works
For every file event declared as enforcing on Claude Code, Gemini CLI,
Cursor IDE, and Kiro CLI, the extractor normalizes the host's Read,
Write, or Edit call to file_read or file_write and emits :read
or :write as the method. The path argument rides along in the call
context; each rule's when: path: glob is matched against it. A matching
deny stops the call before it executes.
Coverage is per event. Codex apply_patch, Codex MCP writes, and
headless codex exec do not provide this enforcement path; Kiro IDE
does not enforce.
What gets blocked
| Agent call | Extracted action | Decision | reason_code |
|---|---|---|---|
Read /etc/passwd | file_read:read | deny | RULE_MATCH |
Read /root/notes.txt | file_read:read | deny | RULE_MATCH |
Read ~/.ssh/id_rsa | file_read:read | deny | RULE_MATCH |
Read ~/.aws/credentials | file_read:read | deny | RULE_MATCH |
Write /etc/hosts | file_write:write | deny | RULE_MATCH |
Read /var/log/app.log (no allow rule) | file_read:read | deny | NO_RULE_MATCH |
What gets allowed
| Agent call | Extracted action | Decision | reason_code |
|---|---|---|---|
Read /tmp/foo.txt | file_read:read | allow | RULE_MATCH |
Write /tmp/scratch.json | file_write:write | allow | RULE_MATCH |
Read /workspace/src/app.py | file_read:read | allow | RULE_MATCH |
Test it yourself
curl -O https://docs.controlzero.ai/recipes/scoped-file-access/policy.yaml
# scenarios.json is not hosted -- build it from the expected-decision table above.
# Requires a Python / Node SDK that ships the tool-extractor spec
# (coming in the 228 Phase 3 release). Confirm with:
# controlzero --version
# Run each documented case with the corresponding tool, method, and JSON arguments:
controlzero test <tool> --method <method> --policy policy.yaml --args '<json>'
Caveats
- Rules match
pathagainst the exact string the hook receives. Different agents pass paths differently: Claude Code passes absolute paths for Read/Write but relative paths for Edit. Point your rules at the form you actually see in the audit log. When in doubt, use a double-prefix pattern (*/.ssh/*catches both/home/me/.ssh/...and~/.ssh/...). - Symbolic links bypass path globs. The agent reading
/tmp/link-to-etc-passwdappears as/tmp/...in the path arg. Mitigate at the OS layer (agent user lacks read permission on/etc/passwd) -- governance is a second line of defense, not the only line. - Bash-wrapped file access (
cat /etc/passwd) routes through theBashextractor, not thefile_readextractor. Pair this recipe with Block outbound network and add explicitBash:cat/Bash:lessrules if that is a concern.