discover — finding skills and applying filters
Every action starts by discovering skills from the configured
SkillSources. This page is about that pipeline:
what gets scanned, how recursion works, the per-source cache, and the
name-collision rules.
pub fn discover_skills(schema: &SkillSchema) -> Result<Vec<RawSkill>, SkillError>
discover_skills iterates the sources in order, discovers each one’s
skills, filters them, and deduplicates — producing the list every action then
works from.
Directory sources
A Directory source is a parent directory whose subdirectories contain
SKILL.md files. The scan is canonicalized first (so the cache key and the
containment checks are stable against symlinked paths), and an unreadable or
non-directory source is a hard InvalidSource error.
Non-recursive (default)
Only the immediate subdirectories of the source are inspected; a
subdirectory is a skill iff it contains a SKILL.md file. Deeper nesting is
ignored:
/home/user/.skills/
├── git/ ← skill (has SKILL.md)
├── docker/ ← skill (has SKILL.md)
└── archive/
└── old-tool/ ← NOT a skill (too deep)
Recursive
With recursive(true), the whole tree under the source is walked: every
directory at any depth that contains a SKILL.md becomes a skill, and the
walk continues below it (a skill directory can also contain nested skill
directories). Cycles are impossible in a real filesystem but the walk keeps a
visited set regardless.
In both modes the skill’s name comes from the name: frontmatter field,
falling back to the directory name.
Embedded sources
An Embedded source is a single in-memory skill, produced directly from the
EmbeddedSkill value. Its content is parsed like
a real SKILL.md: frontmatter fields in the content take precedence over the
struct fields (name, description), globs from the content are used when
non-empty (else the struct’s), and always_apply is OR-ed. The skill’s
“directory” is empty, which is why read_asset cannot work for it.
The discovery cache
Discovered skills are cached per source for the lifetime of the process:
- The cache key is the canonicalized source path (
dir:/abs/path) orembedded:<name>. - On a cache hit the stored skills are cloned; on a miss the source is scanned, cached, and returned.
- The cache is never invalidated: if a
SKILL.mdchanges on disk mid-session (or a skill directory is added or removed), the module keeps serving the snapshot from the first scan of that source.
This is what makes repeated skills_* calls cheap in a long session — the
filesystem is only touched once per source. It also means new skills require
a new process (or a source path change) to appear.
Dedup: first source wins
After discovery, skills are merged across sources in sources order. If two
sources contribute a skill with the same name, the first occurrence wins
and the later one is dropped with a warning log:
duplicate skill name 'git' from source 'dir:/home/user/extra' — keeping first occurrence from 'dir:/home/user/.skills'
Order your sources by priority: the most authoritative source first.
Filters
The two filters apply to skill names after discovery:
| Filter | Rule |
|---|---|
ignore |
Glob-style patterns; a skill whose name matches any pattern is dropped. Patterns support * wildcards ("deprecated-*") plus exact names and the catch-all "*". |
include |
Allowlist of exact names. Empty = everything passes; non-empty = only listed names survive. |
Both are simple name matchers (not path globs) — see
match_util. Filters apply uniformly across all four actions,
so a skill excluded here is invisible to list, read, and match alike.