Read-side access to Claude Code's on-disk skill definitions.
Claude Code resolves user-level skills from
~/.claude/skills/<dir_stem>/SKILL.md. Unlike agents (which are flat
.md files), each skill is a directory containing a SKILL.md
plus optional bundled assets (scripts/, reference/, etc.). The
frontmatter on SKILL.md carries the skill's metadata (name,
description); the body is the skill's instructions.
This module is read-only on purpose -- mutations (create / update / delete) are deferred. Creating a skill is more involved than a file write because it implies directory layout and optional scaffold assets.
Two levels of granularity:
list/1-- enumerate every skill at the root with summary metadata (name, description, dir path, has_assets).get/2-- read one skill's full record including the instructions body.
Frontmatter format
Real-world skills look like:
---
name: recall
description: Search mente for memories by topic, text, tags, or ranked search
---
# Search mente for memories
...The parser is permissive: only name and description are typed.
Any other key: value pairs land in Skill.extra so unknown future
keys survive a round trip. Frontmatter is optional -- a body-only
SKILL.md parses fine, with name defaulting to the directory stem.
Stem, name, directory
By convention a skill's name matches its directory name:
~/.claude/skills/recall/SKILL.md carries name: recall. The two
can diverge -- the parser keeps both. get/2 looks up by directory
stem (because that's what the filesystem indexes), not by the
frontmatter name.
Pointing at a different root
The default is ~/.claude/skills. Pass an explicit path to at/1
to point at a different directory -- a temp dir in tests, a
non-default Claude Code install. The on-disk layout
(<root>/<dir_stem>/SKILL.md) is the same regardless of root.
Example
{:ok, root} = ClaudeWrapper.Skills.home()
{:ok, summaries} = ClaudeWrapper.Skills.list(root)
for s <- summaries do
IO.puts("#{s.dir_stem}: #{s.description}")
end
{:ok, skill} = ClaudeWrapper.Skills.get(root, "recall")
IO.puts(skill.body)
Summary
Functions
Use a specific path as the skills root. Useful for tests (point at a temp dir) and non-default installs.
Read one skill by directory stem (the basename of the <dir_stem>/
directory under the root) into a full Skill.
Resolve the default skills root, ~/.claude/skills.
List every skill at the root, sorted by directory stem.
The configured root directory.
Types
@type t() :: %ClaudeWrapper.Skills{root: String.t()}
Functions
Use a specific path as the skills root. Useful for tests (point at a temp dir) and non-default installs.
@spec get(t(), String.t()) :: {:ok, ClaudeWrapper.Skills.Skill.t()} | {:error, ClaudeWrapper.Error.t()}
Read one skill by directory stem (the basename of the <dir_stem>/
directory under the root) into a full Skill.
Returns {:error, %ClaudeWrapper.Error{kind: :not_found}} when no such
directory exists or it has no SKILL.md.
@spec home() :: {:ok, t()} | {:error, ClaudeWrapper.Error.t()}
Resolve the default skills root, ~/.claude/skills.
Returns {:error, %ClaudeWrapper.Error{kind: :no_home}} when the user
home cannot be determined.
@spec list(t()) :: {:ok, [ClaudeWrapper.Skills.Summary.t()]} | {:error, ClaudeWrapper.Error.t()}
List every skill at the root, sorted by directory stem.
A "skill" is any direct child directory of the root that contains a
SKILL.md. Directories without SKILL.md and non-directory entries
are ignored. Returns {:ok, []} when the root directory does not
exist (a fresh install with no user skills). Directories whose
SKILL.md fails to read are skipped rather than failing the whole
listing.
The configured root directory.