Skip to content

FileSystem

FileSystem gives an agent a fixed set of file tools — read, write, edit, list, search, find, create, and inspect — 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.

Source

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.

FileSystem centralizes those guards. It exposes one bounded, sandboxed toolset so you configure the boundary once and reuse it across agents.

Usage

Add FileSystem to your agent’s capabilities with a root_dir. Everything the agent reads or writes is confined to that directory.

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)

root_dir defaults to the current directory (.), but passing an explicit workspace path is the recommended practice — the sandbox is only as tight as the root you give it.

Tools

FileSystem contributes eight tools, all path-scoped to root_dir:

ToolPurpose
read_fileRead a text file with line numbers and a content hash. Binary files are detected and not dumped. Supports offset/limit paging.
write_fileCreate or overwrite a file. Optional expected_hash rejects stale writes (optimistic concurrency).
edit_fileExact-string replacement; old_text must match exactly once. Optional expected_hash.
list_directoryList a directory’s entries with type indicators and sizes.
search_filesRegex search over file contents, optionally narrowed by an include_glob.
find_filesGlob search over file names (e.g. *.py, **/*.json).
create_directoryCreate a directory and any missing parents.
file_infoMetadata for a file or directory (size, type, line count, hash, symlink target).

Tool errors the model can correct — a missing file, a denied path, a stale edit — are surfaced as ModelRetry, so the agent gets the error message back and can adjust rather than aborting the run.

Security model

  • Containment. Paths resolve relative to root_dir; anything resolving outside — via .., an absolute path, or a symlink — is rejected. Symlinks are resolved with os.path.realpath before the containment check, closing the TOCTTOU window.
  • Binary detection. read_file returns a placeholder instead of dumping binary bytes into the model context.
  • Optimistic concurrency. write_file/edit_file accept an expected_hash so 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 **.

FieldEffect
allowed_patternsIf non-empty, only matching paths are accessible (allowlist).
denied_patternsMatching paths are always rejected (denylist).
protected_patternsMatching 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.

from pydantic_ai import Agent
from pydantic_ai_harness import FileSystem

agent = Agent(
    'anthropic:claude-sonnet-4-6',
    capabilities=[
        FileSystem(
            root_dir='./workspace',
            allowed_patterns=['*.py', '*.toml'],
            denied_patterns=['**/node_modules/*'],
        ),
    ],
)

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 by allowed_patterns — a directory root like . never matches a file pattern such as src/*.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.

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 three integer limits must be positive; they are validated at construction and raise ValueError otherwise.

Agent spec (YAML/JSON)

FileSystem works with Pydantic AI’s agent spec:

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.

Further reading

API reference

FileSystem

Bases: AbstractCapability[AgentDepsT]

File system access scoped to a root directory.

All paths are resolved relative to root_dir. Traversal above the root is rejected. Symlinks are resolved before authorization.

Attributes

root_dir

Root directory for all file operations. Defaults to the current directory.

Type: str | Path Default: '.'

allowed_patterns

If non-empty, only paths matching at least one glob pattern are accessible.

Type: Sequence[str] Default: field(default_factory=(list[str]))

denied_patterns

Paths matching any of these glob patterns are rejected.

Type: Sequence[str] Default: field(default_factory=(list[str]))

protected_patterns

Paths matching these patterns are read-only (writes are rejected).

Defaults to protecting .git/, .env, key files, and secrets. Set to an empty list to disable protection.

Type: Sequence[str] Default: field(default_factory=(lambda: list(_DEFAULT_PROTECTED)))

max_read_lines

Maximum number of lines returned by a single read_file call.

Type: int Default: 2000

max_search_results

Maximum number of matches returned by search_files.

Type: int Default: 1000

max_find_results

Maximum number of matches returned by find_files.

Type: int Default: 1000

Methods

get_toolset
def get_toolset() -> FileSystemToolset[AgentDepsT]

Build and return the filesystem toolset.

Returns

FileSystemToolset[AgentDepsT]