Skills
core/skillgarden/ decides which skill files actually get injected
into context for a given turn — frontmatter parsing, discovery,
activation, and its own budget accounting — not a static "always
include this file" mechanism.
An agent's skillsDirs (see Configure an Agent)
points at one or more folders of SKILL.md files; SkillGarden decides
per turn which of them are relevant enough to load, keeping the ones
that aren't out of context entirely rather than paying their token cost
on every single call.
How lazy loading actually works
Two phases, deliberately split:
- Index now —
buildIndex()scans everySKILL.mdunderskillsDirs, but parses only the frontmatter —nameanddescription, nothing from the body. Entries fill a token budget (skillIndexBudgetTokens, default 2000 — Claude Code itself caps this kind of index around 1% of the window) in order, and whatever doesn't fit is dropped from the index entirely rather than truncated mid-entry. This lightweight index is what actually sits in context on every turn, however many skills exist. - Load later — the full markdown body is only ever read off disk
by
load()/invoke(), and only for the one skill actually being used. A skill nobody invokes this session costs nothing beyond its one index line.
The model doesn't get one tool per skill — that would make the tool
list itself scale with skill count, undoing the whole point. Instead,
one shared Skill tool schema is added whenever skillIndex.length
is non-zero, taking { skill, args }: the skill's own name is an
input, not a separate tool. When the model calls it, run-agent.ts's
loop recognizes that one specific tool name and calls
skillGarden.invoke(skill, args) right then — reading the file,
stripping frontmatter, substituting $ARGUMENTS/$1/$2 placeholders
with args — and answers with the full body as the tool_result
content. This bypasses ActAuth gating and ToolLane scheduling
entirely: a Skill call isn't a real tool call with side effects, it's
a context-injection instruction the model just emitted as one.
A skill can also restrict itself to specific work instead of always
being eligible: frontmatter's optional paths (glob patterns, */?)
only activates it once a touched file in the session matches one of
them — a skill with no paths is always eligible once it fits the
index budget.
Adding a skill
Three ways, in increasing order of how much loopengine does for you:
- Text editor — write
agents/<name>/skills/<id>/SKILL.mdby hand: a----fenced frontmatter block (name,description, optionalpaths) followed by the skill's own markdown body. The only way to reach a nested skill (agents/<name>/skills/deploy/web/SKILL.md, namespaceddeploy:webby discovery) or setpaths— the Web UI form below doesn't expose either. - Web UI, Skills tab — a flat id, description, and body field;
writeSkill()regenerates the frontmatter from those two fields on save. Deliberately scoped to flat (non-nested) skills only, and doesn't preserve any other frontmatter key (likepaths) a hand-written skill might have — editing one through this tab drops them. loopengine add-ability— installs a skill alongside its tool and permission rules as one unit, rather than a skill alone. Right when the skill documents a tool that also needs its ownactauth.ymlentry to be safe to use — see Abilities and Ability System.