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

While Pydantic AI Harness is on 0.x releases, the API may change between minor releases; when it does, deprecation warnings and release-note migration guidance tell you (or your agent) exactly how to upgrade. See the version policy.

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). The pattern is relative to path; absolute patterns are rejected.
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, a directory that collides with an existing file, an invalid glob pattern, a path name rejected by Windows, a path name the filesystem cannot encode, an over-long path name, a symlink loop — are surfaced as ModelRetry, so the agent gets the error message back and can adjust rather than aborting the run. Failures the model can do nothing about, such as a full or read-only disk, still abort.

When an OS error supplies a filename, FileSystem reports it relative to root_dir; paths outside root_dir become <outside-workspace>. file_info applies the same rule to absolute symlink targets.

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, and I/O then uses the resolved path. Directory walks (list_directory, search_files, find_files) resolve each entry the same way and match the patterns against that resolved target, so a symlink cannot name a file outside the tree or present a denied file under a permitted name. These checks are pathname-based: if another process mutates the tree between resolution and I/O, the path read can differ from the path checked.
  • 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.
  • Regular write targets. write_file rejects an existing target that is not a regular file. On POSIX, it opens the final target descriptor in non-blocking mode and checks that descriptor’s type before truncating, so a FIFO at the final component cannot stall the tool even if it is swapped into place during the write.

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 denied 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 walked and each entry is filtered with read-level access against allowed_patterns and denied_patterns. A directory listing cannot surface a path the agent couldn’t otherwise read.

So with allowed_patterns=['*.py'], list_directory('.') succeeds and shows only the .py entries; read_file('notes.md') is rejected.

Matching protected_patterns alone does not hide an entry. Protected paths that pass the allowed, denied, and dotfile filters remain visible to all three walkers and directly readable via read_file/file_info; write operations reject them.

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_list_results=1000,         # cap for list_directory
    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 and raise ValueError otherwise. A walker that hits its cap ends its output with a [... truncated at N ...] marker, and only when a further entry was actually dropped.

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_list_results

Maximum number of entries returned by list_directory.

Type: int Default: 1000

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

read_only

Whether to expose only the tools in READ_ONLY_TOOL_NAMES.

Type: bool Default: False

Methods

get_toolset
def get_toolset() -> FileSystemToolset[AgentDepsT] | FilteredToolset[AgentDepsT]

Build and return the filesystem toolset.

Returns

FileSystemToolset[AgentDepsT] | FilteredToolset[AgentDepsT]