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
The coding-agent hook receives the Read, Write, and Edit tool
calls (and their host-specific aliases) from Claude Code / Cursor /
Codex. The extractor normalizes the tool name 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. Rules evaluate top-down, so the explicit
deny rules fire before any allow, and sensitive paths are cut off
even inside otherwise-allowed trees.
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
curl -O https://docs.controlzero.ai/recipes/scoped-file-access/scenarios.json
# Requires a Python / Node SDK that ships the tool-extractor spec
# (coming in the 228 Phase 3 release). Confirm with:
# controlzero --version
controlzero test-policy policy.yaml --scenarios scenarios.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.