ClaudeWrapper.Agents (ClaudeWrapper v0.8.0)

Copy Markdown View Source

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

Types

Attributes for write/3 and write_new/3.

t()

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

attrs()

@type attrs() :: Enumerable.t()

Attributes for write/3 and write_new/3.

A keyword list or map. All keys are optional:

  • :name -- frontmatter name; defaults to the file stem
  • :description -- frontmatter description
  • :tools -- list of tool names, rendered comma-joined
  • :model -- frontmatter model
  • :body -- the agent prompt body
  • :extra -- map of additional frontmatter key => string pairs, rendered in sorted order after the typed keys

t()

@type t() :: %ClaudeWrapper.Agents{root: String.t()}

Functions

at(path)

@spec at(String.t()) :: t()

Use a specific path as the agents root. Useful for tests (point at a temp dir) and non-default installs.

delete(a, file_stem)

@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.

get(a, file_stem)

@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.

home()

@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.

list(a)

@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.

root(agents)

@spec root(t()) :: String.t()

The configured root directory.

write(a, file_stem, attrs)

@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.

write_new(a, file_stem, attrs)

@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.