FileSystem
Give an agent sandboxed, pattern-filtered access to a directory tree.
The problem
Letting an agent touch the filesystem directly is risky: path traversal
(../../etc/passwd), symlinks that escape the project, clobbering .git, or
leaking .env secrets. Hand-rolling the guards around every tool call is
repetitive and easy to get subtly wrong.
The solution
FileSystem exposes a fixed set of file tools, all scoped to a single
root_dir. Every path is resolved and containment-checked (symlinks included)
before any I/O, and access is filtered through allow / deny / protected glob
patterns.
from pydantic_ai import Agent
from pydantic_ai_harness import FileSystem
agent = Agent(
'anthropic:claude-sonnet-4-6',
capabilities=[FileSystem(root_dir='./workspace')],
)
result = agent.run_sync('Read config.toml and tell me the package name.')
print(result.output)
Tools
| Tool | Purpose |
|---|---|
read_file |
Read a text file with line numbers and a content hash. Binary files are detected and not dumped. Supports offset/limit paging. |
write_file |
Create or overwrite a file. Optional expected_hash rejects stale writes (optimistic concurrency). |
edit_file |
Exact-string replacement; old_text must match exactly once. Optional expected_hash. |
list_directory |
List a directory's entries with type indicators and sizes. |
search_files |
Regex search over file contents, optionally narrowed by an include_glob. |
find_files |
Glob search over file names (e.g. *.py, **/*.json). |
create_directory |
Create a directory and any missing parents. |
file_info |
Metadata for a file or directory (size, type, line count, hash, symlink target). |
Security model
- Containment. Paths resolve relative to
root_dir; anything resolving outside -- via.., an absolute path, or a symlink -- is rejected. Symlinks are resolved withos.path.realpathbefore the containment check, closing the TOCTTOU window. - Binary detection.
read_filereturns a placeholder instead of dumping binary bytes into the model context. - Optimistic concurrency.
write_file/edit_fileaccept anexpected_hashso an agent operating on a stale read is told to re-read rather than silently overwriting newer content.
Pattern filtering
Three independent glob lists control access. Patterns are matched with
fnmatch, whose * spans /, so *.py matches src/main.py and you rarely
need **.
| Field | Effect |
|---|---|
allowed_patterns |
If non-empty, only matching paths are accessible (allowlist). |
denied_patterns |
Matching paths are always rejected (denylist). |
protected_patterns |
Matching paths are read-only -- reads succeed, writes are rejected. |
protected_patterns defaults to .git/*, .env/.env.*, *.pem, *.key,
and **/secrets*. Pass an empty list to disable protection.
Direct access vs. walkers
The three rules apply at two different granularities:
- Direct access (
read_file,write_file,edit_file,file_info,create_directory) gates the operation's target path. You must name a path that the patterns permit. - Walkers (
list_directory,search_files,find_files) gate their root by deny/protected patterns, but not byallowed_patterns-- a directory root like.never matches a file pattern such assrc/*.py, so requiring it to would make every listing fail. Instead, the root is always walked and each entry is filtered against all three lists. A directory listing can never surface a path the agent couldn't otherwise read or write.
So with allowed_patterns=['*.py'], list_directory('.') succeeds and shows
only the .py entries; read_file('notes.md') is rejected.
Note that the walkers filter entries with write-level access, so
protected_patterns matches are omitted from list_directory, search_files,
and find_files output even though those exact paths remain directly readable
via read_file/file_info.
Dotfiles and dot-directories (
.git,.env,.github, ...) are skipped by all three walkers --list_directory,search_files, andfind_files-- regardless of patterns.
Configuration
from pydantic_ai_harness import FileSystem
FileSystem(
root_dir='.', # str | Path -- sandbox root
allowed_patterns=[], # allowlist globs (empty = allow all)
denied_patterns=[], # denylist globs
protected_patterns=[...], # read-only globs (defaults to secrets/.git)
max_read_lines=2000, # cap for a single read_file
max_search_results=1000, # cap for search_files
max_find_results=1000, # cap for find_files
)
The integer limits must be positive; they are validated at construction.
Agent spec (YAML/JSON)
FileSystem works with Pydantic AI's
agent spec:
# agent.yaml
model: anthropic:claude-sonnet-4-6
capabilities:
- FileSystem:
root_dir: ./workspace
allowed_patterns: ['*.py', '*.toml']
from pydantic_ai import Agent
from pydantic_ai_harness import FileSystem
agent = Agent.from_file('agent.yaml', custom_capability_types=[FileSystem])
Pass custom_capability_types so the spec loader knows how to instantiate
FileSystem.