Skip to content

Repo Context

RepoContext discovers and loads a repo’s accumulated coding-assistant context engineering (CE): the instruction files (CLAUDE.md/AGENTS.md) scattered across the tree and the assets under .claude/.agents/.codex/.grok (skills, sub-agents, hooks).

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

A repo accumulates CE for whatever coding assistant worked in it: instruction files (CLAUDE.md/AGENTS.md) scattered across the tree, and assets under .claude/.agents/.codex/.grok (skills, sub-agents, hooks). An agent that loads only the top-level instruction file misses the ancestor context and has no idea the rest of the setup exists, so it cannot honor all of it.

The solution

RepoContext bundles three strategies, each independently toggleable. Construct it with RepoContext(...) in an Agent’s capabilities, with a workspace attached to the run:

from pathlib import Path

from pydantic_ai import Agent
from pydantic_ai.capabilities import LocalWorkspace
from pydantic_ai_harness import RepoContext

agent = Agent(
    'anthropic:claude-opus-5-5',
    capabilities=[LocalWorkspace('.'), RepoContext(home_dir=Path.home())],
)

result = agent.run_sync('Summarize the coding-assistant setup in this repo.')
print(result.output)

RepoContext reads from the run’s workspace, starting at its working directory. To point it at a subdirectory, set it on the workspace: LocalWorkspace('./repo'). home_dir and asset_roots are paths in the workspace: relative ones resolve from the working directory, and ~ is not expanded. A run without a workspace fails at its start.

1. Walk-up instruction autoload (on by default)

Loads CLAUDE.md/AGENTS.md from the working directory and every ancestor up to home_dir (inclusive). Precedence is ancestor-first, workspace-last: broadest context first, most specific last. Files are deduped by visited path and by content hash, so a symlinked AGENTS.md -> CLAUDE.md or two ancestors sharing identical content load once. Symlinks to instruction files outside the directory being scanned are skipped, including shared dotfiles in another directory.

When home_dir is None (the default), only the working directory is scanned — no walk-up. Pass the workspace home path explicitly to walk up to it. For a remote sandbox, use its home (for example home_dir='/home/user' on E2B), not the agent host’s Path.home().

2. Asset inventory (off by default)

Set expose_inventory_tool=True to expose inventory_agent_context(), which reports where the repo’s CE assets live — the .claude/.agents/.codex/.grok roots and, within each, the skills/ (SKILL.md), agents/ (.md), and settings.json (hooks) it contains. It returns a structured AgentContextInventory; it locates assets and does not parse them.

Rename the tool with inventory_tool_name, or scope which roots it scans with asset_roots. Skill discovery goes at most eight directories deep.

3. Nested-on-traversal (off by default)

When the model lists or reads a directory, surface that directory’s CLAUDE.md/AGENTS.md. This strategy subscribes to FileReadEvent and DirectoryListedEvent, so it receives normalized, containment-checked paths instead of inspecting raw tool arguments. It remains opt-in:

from pydantic_ai import Agent
from pydantic_ai.capabilities import LocalWorkspace
from pydantic_ai_harness import FileSystem, RepoContext

agent = Agent(
    'anthropic:claude-opus-5-5',
    capabilities=[
        LocalWorkspace('.'),
        FileSystem(),
        RepoContext(
            nested_traversal=True,
            nested_inject='pointer',  # or 'contents'
        ),
    ],
)
result = agent.run_sync('List the source directory.')

nested_inject='pointer' (default) enqueues a one-line note pointing at the file; 'contents' enqueues the file body. The note reaches message history before the next model request. Each directory is surfaced at most once per run.

FileSystem emits these events directly. Hosts with other file tools can emit the same types by importing FileReadEvent and DirectoryListedEvent from pydantic_ai_harness.filesystem; set root_dir to the directory the event’s path is relative to.

The traversed location is root_dir / path, so a FileSystem rooted at a subdirectory of the working directory still surfaces the right directory. A traversal that resolves outside the working directory is ignored: it is not nested in the workspace, so there is no nested context to surface.

traversal_tool_names and traversal_path_arg are deprecated. Setting either to a non-default value emits HarnessDeprecationWarning and keeps the old tool-name and argument sniffing path active for hosts that do not emit events. With the defaults, sniffing is disabled, so a FileSystem event cannot deliver the same note twice.

Cache cost

Injecting file contents into the system prompt costs prompt-cache stability: a changed prefix re-bills the whole cached region. RepoContext keeps the two cache-relevant paths separate:

  • Strategy 1 reads its files once at run start and injects them as static system instructions, so the cached prefix stays byte-identical across turns.
  • Strategy 3 is volatile (it depends on which directory was just touched), so its note is enqueued in the message tail, never in the system prompt, and cannot invalidate the cached prefix.

Configuration

RepoContext(
    home_dir=None,                  # str | Path | None -- shallowest workspace dir to stop walk-up at, inclusive
    filenames=('CLAUDE.md', 'AGENTS.md'),
    autoload_instructions=True,     # Strategy 1
    expose_inventory_tool=False,    # Strategy 2
    inventory_tool_name='inventory_agent_context',
    nested_traversal=False,         # Strategy 3
    nested_inject='pointer',        # 'pointer' | 'contents'
    traversal_tool_names=frozenset({'list_directory', 'read_file'}),  # deprecated fallback
    traversal_path_arg='path',                                       # deprecated fallback
    asset_roots=('.claude', '.agents', '.codex', '.grok'),
)

workspace_dir= is deprecated and ignored: the working directory comes from the workspace.

Scope

RepoContext locates and loads CE; it does not parse skill/sub-agent frontmatter or hook bodies, and it does not modify assets. Strategy 1 reads its files once per run, so mid-run edits to those files are not reloaded.

Further reading

API reference

RepoContext

Bases: AbstractCapability[AgentDepsT]

Discover and load a repo’s accumulated coding-assistant context engineering.

Three strategies, each independently toggleable:

Everything is read through the run’s workspace, anchored at its working directory; a run without a workspace fails at its start.

  1. Walk-up instruction autoload (autoload_instructions, on by default): load CLAUDE.md/AGENTS.md from the working directory and every ancestor up to home_dir, deduped, ancestor-first. These are read once at run start and injected as static system instructions via get_instructions, so they stay in the cached prefix and never re-read per turn.

  2. Asset inventory (expose_inventory_tool, off by default): a tool that reports where the repo’s CE assets live (.claude/.agents/.codex/ .grok and their skills/, agents/, settings.json). It locates assets; it does not parse them.

  3. Nested-on-traversal (nested_traversal, off by default): when the model lists or reads a directory through a filesystem capability event, surface that directory’s CLAUDE.md/AGENTS.md. The note is enqueued in the message tail, not added to system instructions, so it does not invalidate the cached prefix. nested_inject='pointer' (default) enqueues a one-line pointer; 'contents' inlines the file body.

Cache note: injecting file contents into the system prompt costs prompt-cache stability. Strategy 1 is safe because its files are static; the volatile Strategy 3 content rides in the message tail instead.

from pathlib import Path

from pydantic_ai import Agent
from pydantic_ai.capabilities import LocalWorkspace
from pydantic_ai_harness.repo_context import RepoContext

agent = Agent(
    'anthropic:claude-sonnet-4-6',
    capabilities=[LocalWorkspace('.'), RepoContext(home_dir=Path.home())],
)

Attributes

workspace_dir

Deprecated and ignored: the walk-up and asset scan are anchored at the workspace’s working directory.

Set the working directory on the workspace instead, e.g. LocalWorkspace('./repo').

Type: Path | None Default: None

home_dir

The shallowest workspace directory to stop the walk-up at, inclusive, as a workspace path.

A relative path resolves against the working directory. None (the default) scans only the working directory — no walk-up.

Type: str | Path | None Default: None

filenames

Instruction filenames to look for, in within-directory precedence order.

Type: Sequence[str] Default: ('CLAUDE.md', 'AGENTS.md')

autoload_instructions

Strategy 1: load instruction files into the system prompt.

Type: bool Default: True

expose_inventory_tool

Strategy 2: expose the asset-inventory tool.

Type: bool Default: False

inventory_tool_name

Name of the inventory tool exposed to the model.

Type: str Default: 'inventory_agent_context'

nested_traversal

Strategy 3: surface a directory’s instruction file when the model lists or reads that directory. Off by default — it couples to the list/read tools.

Type: bool Default: False

nested_inject

For Strategy 3: append a one-line pointer, or inline the file contents.

Type: Literal[‘pointer’, ‘contents’] Default: 'pointer'

traversal_tool_names

Deprecated tool names used by the compatibility traversal detector.

Type: frozenset[str] Default: _DEFAULT_TRAVERSAL_TOOL_NAMES

traversal_path_arg

Deprecated path argument used by the compatibility traversal detector.

Type: str Default: _DEFAULT_TRAVERSAL_PATH_ARG

asset_roots

Root directories the inventory tool scans, relative to the working directory.

Type: Sequence[str] Default: ('.claude', '.agents', '.codex', '.grok')

Methods

for_run

@async

def for_run(ctx: RunContext[AgentDepsT]) -> RepoContext[AgentDepsT]

Return a fresh per-run instance with isolated traversal/cache state.

The copy skips __post_init__, so deprecated arguments warned about at construction are not warned again.

Returns

RepoContext[AgentDepsT]

before_run

@async

def before_run(ctx: RunContext[AgentDepsT]) -> None

Fail without a workspace, and load walk-up instruction files so get_instructions is sync.

Returns

None

get_instructions
def get_instructions() -> str | Callable[[RunContext[AgentDepsT]], str | None] | None

Cache-stable instructions resolved after before_run loads workspace files.

Returns

str | Callable[[RunContext[AgentDepsT]], str | None] | None

get_toolset
def get_toolset() -> AgentToolset[AgentDepsT] | None

The asset-inventory toolset, or None when the tool is disabled.

Returns

AgentToolset[AgentDepsT] | None

after_tool_execute

@async

def after_tool_execute(
    ctx: RunContext[AgentDepsT],
    *,
    call: ToolCallPart,
    tool_def: ToolDefinition,
    args: dict[str, Any],
    result: Any,
) -> Any

Support customized legacy traversal tool and argument names.

Returns

Any

get_serialization_name

@classmethod

def get_serialization_name(cls) -> str | None

Serialization name for agent-spec support.

Returns

str | None

AgentContextInventory

Bases: BaseModel

A map of where a repo’s CE assets live, for an orchestrator to inspect.

AssetRoot

Bases: BaseModel

Where CE assets live under a single root directory (e.g. .claude).