Claude Code-style skills with progressive disclosure.
A skill is a directory containing a SKILL.md markdown file with YAML
frontmatter. The frontmatter is cheap (a sentence) and is injected into
the system prompt as a one-line catalog entry. The body is loaded into
context only when the model decides it needs the skill — either by
emitting a [skill: <name>] sentinel in its response, or by the host
pre-attaching it via preload/2.
This means dozens of skills can be available at ~50 tokens each in catalog form; only the ones the model actually wants pay the full body cost.
Layout
~/.config/ex_athena/skills/<name>/SKILL.md # user-level skills
<cwd>/.exathena/skills/<name>/SKILL.md # project-level skillsProject skills override user skills with the same name.
Frontmatter schema
---
name: my-skill
description: short description used in the catalog
disable-model-invocation: false
allowed-tools: [read, glob, grep]
---
# Body
…whatever instructions the agent should follow when this skill is
active. Anthropic recommends keeping bodies under 500 lines and
splitting into linked files for anything larger.Only name and description are required. disable-model-invocation
hides the skill from the catalog (host can still preload/2 it).
allowed-tools (when set) restricts the tool list while the skill is
loaded; PR3a wires this into Permissions.check/4.
Catalog rendering
Skills.catalog_section([%Skill{name: "deploy", description: "Deploy
this app to staging"}, ...])
#=>
## Available Skills
Use `[skill: <name>]` to load a skill's full instructions.
- `deploy` — Deploy this app to staging
Summary
Functions
Build a system-role message that activates skill_name from skills.
Render the catalog section that's appended to the system prompt. Empty string when no model-invocable skills exist (so we don't pollute the prompt with a bare header).
Discover all skills available for a given working directory. Returns a map keyed by skill name; later sources override earlier ones (project beats user).
Extracts skill names referenced via [skill: <name>] sentinels in a
block of model output. De-duplicated; case-sensitive on the name.
Returns the set of skill names already activated in messages (so we
don't re-attach idempotently).
Pre-load a list of skill bodies onto a message list. Returns the amended message list. Idempotent — already-loaded skills are skipped.
Functions
@spec activation_message(map(), String.t()) :: {:ok, ExAthena.Messages.Message.t()} | {:error, :not_found}
Build a system-role message that activates skill_name from skills.
Returns {:ok, message} when the skill exists, {:error, :not_found}
otherwise. The message is tagged name: "skill:<name>" so we can
detect already-loaded skills (idempotency) and so the compactor knows
not to drop it.
@spec catalog_section(map() | [ExAthena.Skills.Skill.t()]) :: String.t()
Render the catalog section that's appended to the system prompt. Empty string when no model-invocable skills exist (so we don't pollute the prompt with a bare header).
@spec discover( String.t(), keyword() ) :: %{required(String.t()) => ExAthena.Skills.Skill.t()}
Discover all skills available for a given working directory. Returns a map keyed by skill name; later sources override earlier ones (project beats user).
Options
:user_dir— override the user-level skills directory.:project_dir— override the project-level skills directory (default:<cwd>/.exathena/skills).
Extracts skill names referenced via [skill: <name>] sentinels in a
block of model output. De-duplicated; case-sensitive on the name.
@spec loaded_skills([ExAthena.Messages.Message.t()]) :: MapSet.t()
Returns the set of skill names already activated in messages (so we
don't re-attach idempotently).
@spec preload([ExAthena.Messages.Message.t()], map(), [String.t()]) :: [ ExAthena.Messages.Message.t() ]
Pre-load a list of skill bodies onto a message list. Returns the amended message list. Idempotent — already-loaded skills are skipped.
Useful for hosts that know up-front which skills the agent will need
(e.g. a /deploy slash command pre-attaching the deploy skill so
the agent doesn't have to discover it).