ClaudeWrapper.Skills (ClaudeWrapper v0.8.0)

Copy Markdown View Source

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

t()

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

Functions

at(path)

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

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

get(s, dir_stem)

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

home()

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

list(s)

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

root(skills)

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

The configured root directory.