File-backed read/write access to Claude Code's on-disk agent definitions.
Claude Code resolves user-level agents from
~/.claude/agents/<stem>.md. Each file is plain markdown with a
YAML-style frontmatter block delimited by --- lines. The
frontmatter carries the agent's metadata (name, description, optional
tool allow-list, optional model); the body is the agent's system
prompt.
This is distinct from ClaudeWrapper.Commands.Agents, which shells
out to claude agents. This module reads and writes the files
directly.
Two levels of granularity for reads:
list/1-- enumerate every agent at the root with summary metadata (name, description, tools, model, file path).get/2-- read one agent's full record including the prompt body and unknown frontmatter keys.
Plus write/3, write_new/3, and delete/2 for mutations.
Frontmatter format
Real-world agents look like:
---
name: auditor
description: Multi-line folded description...
tools: Read, Glob, Grep, Bash
model: sonnet
---
You are an auditor. ...The parser is permissive: only name, description, tools, and
model are typed. tools is a comma-separated list. Any other
key: value pairs land in Definition.extra so unknown future keys
survive a round trip. Frontmatter is optional -- a body-only file
parses fine, with name defaulting to the file stem.
List-valued and folded frontmatter keys (anything this parser cannot
reduce to a single inline scalar) are tolerated by capturing their
raw inline value into extra rather than failing the parse.
File stem vs name
By convention an agent's name matches its filename stem:
auditor.md carries name: auditor. The two can diverge -- the
parser keeps both. Lookup, write, and delete all key on the file
stem (that's what the filesystem indexes), not the frontmatter
name.
Example
{:ok, root} = ClaudeWrapper.Agents.home()
{:ok, summaries} = ClaudeWrapper.Agents.list(root)
for s <- summaries do
IO.puts("#{s.file_stem}: #{s.description}")
end
{:ok, agent} = ClaudeWrapper.Agents.get(root, "auditor")
IO.puts(agent.body)
Summary
Functions
Use a specific path as the agents root. Useful for tests (point at a temp dir) and non-default installs.
Remove the <file_stem>.md agent.
Read one agent by file stem (the basename of <stem>.md under the
root) into a full Definition.
Resolve the default agents root, ~/.claude/agents.
List every *.md agent at the root, sorted by file stem.
The configured root directory.
Write (create or overwrite) an agent at <file_stem>.md.
Like write/3 but returns {:error, %ClaudeWrapper.Error{kind: :already_exists}} when the agent already exists. Useful for
create-only flows where overwriting would be a bug.
Types
@type attrs() :: Enumerable.t()
Attributes for write/3 and write_new/3.
A keyword list or map. All keys are optional:
:name-- frontmattername; defaults to the file stem:description-- frontmatterdescription:tools-- list of tool names, rendered comma-joined:model-- frontmattermodel:body-- the agent prompt body:extra-- map of additional frontmatterkey => stringpairs, rendered in sorted order after the typed keys
@type t() :: %ClaudeWrapper.Agents{root: String.t()}
Functions
Use a specific path as the agents root. Useful for tests (point at a temp dir) and non-default installs.
@spec delete(t(), String.t()) :: :ok | {:error, ClaudeWrapper.Error.t()}
Remove the <file_stem>.md agent.
Returns {:error, %ClaudeWrapper.Error{kind: :not_found}} when no such
file exists.
@spec get(t(), String.t()) :: {:ok, ClaudeWrapper.Agents.Definition.t()} | {:error, ClaudeWrapper.Error.t()}
Read one agent by file stem (the basename of <stem>.md under the
root) into a full Definition.
Returns {:error, %ClaudeWrapper.Error{kind: :not_found}} when no such
file exists.
@spec home() :: {:ok, t()} | {:error, ClaudeWrapper.Error.t()}
Resolve the default agents root, ~/.claude/agents.
Returns {:error, %ClaudeWrapper.Error{kind: :no_home}} when the user
home cannot be determined.
@spec list(t()) :: {:ok, [ClaudeWrapper.Agents.Summary.t()]} | {:error, ClaudeWrapper.Error.t()}
List every *.md agent at the root, sorted by file stem.
Returns {:ok, []} when the root directory does not exist (a fresh
install with no user agents). Files that fail to parse are skipped
rather than failing the whole listing.
The configured root directory.
@spec write(t(), String.t(), attrs()) :: :ok | {:error, ClaudeWrapper.Error.t()}
Write (create or overwrite) an agent at <file_stem>.md.
Creates the agents root directory if it does not exist. See attrs/0
for the accepted attributes. To fail when the agent already exists
instead of overwriting, use write_new/3.
Returns {:error, %ClaudeWrapper.Error{kind: :invalid_stem}} (with the
stem in :reason) for empty, ., .., or stems containing slashes
or NUL bytes.
@spec write_new(t(), String.t(), attrs()) :: :ok | {:error, ClaudeWrapper.Error.t()}
Like write/3 but returns {:error, %ClaudeWrapper.Error{kind: :already_exists}} when the agent already exists. Useful for
create-only flows where overwriting would be a bug.