一座老城里的回春堂,进门一整面墙都是抽屉。深褐色的木格子,一格挨一格,从地面一直码到房梁,少说也有几百个。每个抽屉脸上只贴一条窄窄的纸签,上面是先生当年亲笔写下的:药名,外加半句话——「清热」、「安神,睡前用」、「理气,孕妇忌」。签子就那么宽,写不下整张方子,也从没人指望它写得下。
抓药的小学徒新来时,师父没让他背药性,只让他把这面墙的纸签从头到尾认一遍。「先认得门,」师父说,「用到哪味,再开哪格。」——他不会把几百个抽屉一股脑全拉开摊在案上,那样满屋子药材,反倒一味也找不着了。客人报上症候,他眼睛在墙上一扫,凭那半句话定位到该开的那一格,这才伸手把抽屉拉开。
抽屉拉开,里头才是真东西:足斤足两的药材,还压着一张叠好的用法纸,写满了炮制、配伍、剂量、禁忌——纸签上那半句话省下的所有细节,全在这里。要用,才取;不用,它就一直叠着,半个字也不占案头。
墙面就那么大,纸签的总长度是有数的。寻常药材,签上只容得下药名加一句话,长了就截断,末尾点上省略号。可有几格是镇店的祖传常用药——这些签子无论墙面多挤,师父都吩咐留足,整句写全,绝不许削。真到了寸土必争的时候,寻常抽屉的签子干脆只剩个光秃秃的药名,而那几格祖传药,照旧字字俱全。
后来铺子带了第二个、第三个学徒。师父没让他们共用一张认门的单子——每人发一张自己的,各认各的墙,免得甲认过的、乙就当自己也认过,反倒漏看。还有些稀罕药材,墙上压根没有抽屉,得到时现往城外的老号去取——平时连签子都不挂,唯有客人点到,才差人快马取回。
让回春堂几百味药不乱的,从来不是把所有抽屉都敞开,而是这条规矩:墙上只挂一条薄薄的名签供认门,真要用哪味,才拉开那一格、取出压在底下的整张用法。这条「先以薄签示其名、临用方启屉取其详」的规矩,正是技能的渐进式披露(progressive disclosure)。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Skills — progressive disclosure (turn-0 catalog, full body loaded on invoke)
Capabilities · 能力扩展
中文速览 · Quick read
A skill in Claude Code is a packaged capability: a markdown document whose frontmatter (description, allowed-tools, when_to_use, model, effort, context) is parsed into a Command object, with its body kept as markdownContent to be expanded later (claude-code/src/skills/loadSkillsDir.ts:185-265, claude-code/src/skills/loadSkillsDir.ts:270-399). What lets a skill library scale is not the file format but the delivery. The model is never shown the full text of every skill up front. Instead the harness uses two phases: first a discovery listing — each skill reduced to its name plus a length-capped one-liner, injected at turn 0 (claude-code/src/tools/SkillTool/prompt.ts:25-29); then the full markdown body, loaded from disk only at the moment the model actually invokes a skill through the SkillTool (claude-code/src/tools/SkillTool/SkillTool.ts:635-643). Discovery shows breadth for almost nothing; capability is paid for only on need. The narrow drawer label versus the full instructions folded inside the drawer.
How it works
getSkillListingAttachments assembles the candidate set. It first short-circuits: an agent whose options.tools does not include the SkillTool gets no listing at all, since it cannot invoke skills anyway (claude-code/src/utils/attachments.ts:2668-2673). Otherwise it collects model-invocable prompt commands via getSkillToolCommands — prompt-type, not disableModelInvocation, not builtin, and either a first-party skill source or carrying an explicit description (claude-code/src/commands.ts:561-581) — merged with MCP-provided skills, which must be prompt-type, loadedFrom === 'mcp', and model-invocable (claude-code/src/commands.ts:547-559).
That candidate list is then squeezed into a character budget. The budget is deliberately tiny: 1% of the model's context window in characters, falling back to 8,000 chars when the window is unknown (claude-code/src/tools/SkillTool/prompt.ts:20-23). formatCommandsWithinBudget first tries full descriptions; if they fit, it ships them as-is (claude-code/src/tools/SkillTool/prompt.ts:70-171). When they overflow, it partitions skills into bundled (Anthropic-curated, never truncated) and the rest, reserves the bundled skills' full text, and divides the remaining budget evenly across the rest (claude-code/src/tools/SkillTool/prompt.ts:92-114). In the extreme case where the per-skill share falls below the 20-char minimum, non-bundled skills collapse to names only while bundled ones keep their full descriptions (claude-code/src/tools/SkillTool/prompt.ts:123-142). Every entry is independently capped at 250 chars, because the listing exists only for discovery — verbose when_to_use strings would waste turn-1 cache_creation tokens without improving the match rate (claude-code/src/tools/SkillTool/prompt.ts:25-29).
Two refinements keep the listing honest. The set of already-announced skills is tracked in a Map keyed by agentId, so each subagent receives its own turn-0 listing rather than inheriting (and thus deduplicating to empty) the main thread's (claude-code/src/utils/attachments.ts:2603-2607). And on --resume, when a skill_listing already exists in the transcript, suppressNextSkillListing marks the current skills as sent so the ~600-token block is not re-injected (claude-code/src/utils/attachments.ts:2633-2635). When experimental skill-search is on, filterToBundledAndMcp narrows the initial listing to just bundled + MCP sources — small and intent-signaled — and falls back to bundled-only if even that exceeds 30 entries, leaving the long tail of user/project/plugin skills to asynchronous discovery (claude-code/src/utils/attachments.ts:2685-2697, claude-code/src/utils/attachments.ts:2638-2659). The result is emitted as a skill_listing attachment carrying the formatted content, a skill count, and an isInitial flag (claude-code/src/utils/attachments.ts:2661-2751). The SkillTool prompt tells the model that these skills appear in system-reminder messages and must be invoked through the tool, never merely mentioned (claude-code/src/tools/SkillTool/prompt.ts:173-196).
Only now is the drawer opened. validateInput checks that the skill name resolves to an existing, model-invocable, prompt-type command (claude-code/src/tools/SkillTool/SkillTool.ts:354-430). call records usage for ranking (claude-code/src/tools/SkillTool/SkillTool.ts:619), then branches. A forked skill (context === 'fork') runs in an isolated sub-agent with its own token budget, inherited tools, model override, and effort, streaming progress back to the parent (claude-code/src/tools/SkillTool/SkillTool.ts:122-289). The default inline path calls processPromptSlashCommand, which is where the full markdown is finally read from disk (claude-code/src/tools/SkillTool/SkillTool.ts:635-643); the command's getPromptForCommand prepends the skill's base directory, substitutes $ARGUMENTS/${CLAUDE_SKILL_DIR}/${CLAUDE_SESSION_ID}, and — except for untrusted MCP skills — executes any inline shell commands before returning the expanded text (claude-code/src/skills/loadSkillsDir.ts:270-399). The expanded content is injected as newMessages, and a contextModifier chains the skill's allowed-tools, model override, and effort level onto the live context (claude-code/src/tools/SkillTool/SkillTool.ts:775-839). The loaded content is registered with the compaction-preservation state via addInvokedSkill, keyed by skill path, so a compaction event can re-inject it later (claude-code/src/tools/SkillTool/SkillTool.ts:1088-1093). Telemetry logs the execution context (inline/forked/remote) and source for every invocation (claude-code/src/tools/SkillTool/SkillTool.ts:152-203). Remote canonical skills (_canonical_<slug>, an ant-only experiment) bypass the local registry entirely and load their SKILL.md from AKI/GCS with caching (claude-code/src/tools/SkillTool/SkillTool.ts:605-612) — the rare herb that has no drawer on the wall and is fetched from afar only when ordered.
Why it matters
A full skill library can run to tens of thousands of tokens; pasting all of it into every turn would crowd out the user's actual work and inflate cache_creation on turn one. The two-phase split decouples how many skills exist from how many tokens they cost when idle. Discovery is bounded to 1% of the window (claude-code/src/tools/SkillTool/prompt.ts:20-23) and each entry hard-capped at 250 chars (claude-code/src/tools/SkillTool/prompt.ts:25-29), so adding skills grows the catalog without proportionally growing the standing context tax — full capability is materialized lazily, exactly once, at invocation (claude-code/src/tools/SkillTool/SkillTool.ts:635-643).
The truncation policy encodes a priority: Anthropic-curated bundled skills keep full descriptions even under budget pressure, and only the long tail degrades — first to trimmed descriptions, then to names-only (claude-code/src/tools/SkillTool/prompt.ts:92-114, claude-code/src/tools/SkillTool/prompt.ts:123-142). Per-agent tracking guarantees every subagent still gets a turn-0 catalog instead of silently inheriting an empty one (claude-code/src/utils/attachments.ts:2603-2607), while resume-suppression avoids paying for the same listing twice across process respawns (claude-code/src/utils/attachments.ts:2633-2635). And because invoked content is preserved in compaction state (claude-code/src/tools/SkillTool/SkillTool.ts:1088-1093), a skill that has done real work is not silently forgotten when history is squeezed. The net effect: the model sees a wide menu cheaply, commits tokens only to the one skill it chooses, and that choice survives the rest of the conversation.
Read the source
- The two-phase contract — listing for discovery, body loaded on invoke: claude-code/src/tools/SkillTool/prompt.ts:25-29, claude-code/src/tools/SkillTool/SkillTool.ts:635-643.
- The discovery budget — 1% of context (8K fallback) and the budget-aware formatter: claude-code/src/tools/SkillTool/prompt.ts:20-23, claude-code/src/tools/SkillTool/prompt.ts:70-171.
- Truncation policy — bundled never trimmed, names-only under extreme pressure: claude-code/src/tools/SkillTool/prompt.ts:92-114, claude-code/src/tools/SkillTool/prompt.ts:123-142.
- Candidate sourcing — local skills and MCP skills filters: claude-code/src/commands.ts:561-581, claude-code/src/commands.ts:547-559.
- Listing assembly — per-agent tracking, skill-search filtering, the
skill_listingattachment, and the SkillTool prompt: claude-code/src/utils/attachments.ts:2661-2751, claude-code/src/utils/attachments.ts:2603-2607, claude-code/src/utils/attachments.ts:2633-2635, claude-code/src/utils/attachments.ts:2638-2659, claude-code/src/utils/attachments.ts:2668-2673, claude-code/src/utils/attachments.ts:2685-2697, claude-code/src/tools/SkillTool/prompt.ts:173-196. - Invocation — validate, inline expansion, forked sub-agent, remote canonical, usage tracking, context modifiers, compaction preservation, telemetry: claude-code/src/tools/SkillTool/SkillTool.ts:354-430, claude-code/src/tools/SkillTool/SkillTool.ts:122-289, claude-code/src/tools/SkillTool/SkillTool.ts:605-612, claude-code/src/tools/SkillTool/SkillTool.ts:619, claude-code/src/tools/SkillTool/SkillTool.ts:775-839, claude-code/src/tools/SkillTool/SkillTool.ts:1088-1093, claude-code/src/tools/SkillTool/SkillTool.ts:152-203.
- Skill parsing and on-demand prompt building: claude-code/src/skills/loadSkillsDir.ts:185-265, claude-code/src/skills/loadSkillsDir.ts:270-399.
classDiagram
class Command {
+description
+allowedTools
+markdownContent lazyBody
}
class SkillListing {
<<turn-0 discovery>>
+budget onePct_or_8000
+capPerEntry 250chars
+bundledNeverTrimmed
+namesOnlyUnderPressure
+formatCommandsWithinBudget()
}
class SkillTool {
+validateInput()
+call()
}
class Invocation {
+processPromptSlashCommand readsBody
+forkedSubagent
+contextModifier toolsModelEffort
+addInvokedSkill compaction
}
Command ..> SkillListing : name plus capped desc
SkillListing ..> SkillTool : model invokes by name
SkillTool ..> Invocation : call
Invocation ..> Command : loads full body on invoke 读法:两段式渐进披露。turn-0 清单只发名字+一句话,预算 = 窗口 1%(未知退回 8000),
每条封顶 250 字(prompt.ts:20-29);溢出时 bundled 全留、长尾先砍描述再砍成只剩名字
(:92-142)。模型按名调用后,processPromptSlashCommand 才读盘展开完整正文
(SkillTool.ts:635),并把 allowed-tools/model/effort 接到 context(:775),登记进压缩保留
(:1088)。
抽屉墙上,每个抽屉只露一张窄标签(名字 + 一句话,≤250 字),完整说明书折在抽屉里。 turn-0 只发标签清单,预算死死卡在窗口的 1%。拖动滑杆把技能库做大:看清单成本 几乎不涨,而「把每份完整正文都贴上来」却线性爆炸。预算紧张时,bundled 官方技能保住全描述, 长尾先砍成只剩名字。点一个抽屉的 invoke —— 完整正文这才从磁盘读进来,只此一次。
清单预算 = 1% 窗口字符(未知时退回 8000,prompt.ts:20-23),每条独立封顶 250 字——清单只为 发现,啰嗦的 when_to_use 只会白烧 turn-1 cache_creation(prompt.ts:25-29)。溢出时 bundled 全留、其余均分预算(prompt.ts:92-114),极端时长尾退化为只剩名字(prompt.ts:123-142)。 完整正文只在 invoke 那一刻读盘(SkillTool.ts:635-643)。
A skill in Claude Code is a packaged capability: a markdown document whose frontmatter (description, allowed-tools, when_to_use, model, effort, context) is parsed into a Command object, with its body kept as markdownContent to be expanded later (claude-code/src/skills/loadSkillsDir.ts:185-265, claude-code/src/skills/loadSkillsDir.ts:270-399). What lets a skill library scale is not the file format but the delivery. The model is never shown the full text of every skill up front. Instead the harness uses two phases: first a discovery listing — each skill reduced to its name plus a length-capped one-liner, injected at turn 0 (claude-code/src/tools/SkillTool/prompt.ts:25-29); then the full markdown body, loaded from disk only at the moment the model actually invokes a skill through the SkillTool (claude-code/src/tools/SkillTool/SkillTool.ts:635-643). Discovery shows breadth for almost nothing; capability is paid for only on need. The narrow drawer label versus the full instructions folded inside the drawer.
How it works
Phase one — the listing. getSkillListingAttachments assembles the candidate set. It first short-circuits: an agent whose options.tools does not include the SkillTool gets no listing at all, since it cannot invoke skills anyway (claude-code/src/utils/attachments.ts:2668-2673). Otherwise it collects model-invocable prompt commands via getSkillToolCommands — prompt-type, not disableModelInvocation, not builtin, and either a first-party skill source or carrying an explicit description (claude-code/src/commands.ts:561-581) — merged with MCP-provided skills, which must be prompt-type, loadedFrom === 'mcp', and model-invocable (claude-code/src/commands.ts:547-559).
That candidate list is then squeezed into a character budget. The budget is deliberately tiny: 1% of the model's context window in characters, falling back to 8,000 chars when the window is unknown (claude-code/src/tools/SkillTool/prompt.ts:20-23). formatCommandsWithinBudget first tries full descriptions; if they fit, it ships them as-is (claude-code/src/tools/SkillTool/prompt.ts:70-171). When they overflow, it partitions skills into bundled (Anthropic-curated, never truncated) and the rest, reserves the bundled skills' full text, and divides the remaining budget evenly across the rest (claude-code/src/tools/SkillTool/prompt.ts:92-114). In the extreme case where the per-skill share falls below the 20-char minimum, non-bundled skills collapse to names only while bundled ones keep their full descriptions (claude-code/src/tools/SkillTool/prompt.ts:123-142). Every entry is independently capped at 250 chars, because the listing exists only for discovery — verbose when_to_use strings would waste turn-1 cache_creation tokens without improving the match rate (claude-code/src/tools/SkillTool/prompt.ts:25-29).
Two refinements keep the listing honest. The set of already-announced skills is tracked in a Map keyed by agentId, so each subagent receives its own turn-0 listing rather than inheriting (and thus deduplicating to empty) the main thread's (claude-code/src/utils/attachments.ts:2603-2607). And on --resume, when a skill_listing already exists in the transcript, suppressNextSkillListing marks the current skills as sent so the ~600-token block is not re-injected (claude-code/src/utils/attachments.ts:2633-2635). When experimental skill-search is on, filterToBundledAndMcp narrows the initial listing to just bundled + MCP sources — small and intent-signaled — and falls back to bundled-only if even that exceeds 30 entries, leaving the long tail of user/project/plugin skills to asynchronous discovery (claude-code/src/utils/attachments.ts:2685-2697, claude-code/src/utils/attachments.ts:2638-2659). The result is emitted as a skill_listing attachment carrying the formatted content, a skill count, and an isInitial flag (claude-code/src/utils/attachments.ts:2661-2751). The SkillTool prompt tells the model that these skills appear in system-reminder messages and must be invoked through the tool, never merely mentioned (claude-code/src/tools/SkillTool/prompt.ts:173-196).
Phase two — invocation. Only now is the drawer opened. validateInput checks that the skill name resolves to an existing, model-invocable, prompt-type command (claude-code/src/tools/SkillTool/SkillTool.ts:354-430). call records usage for ranking (claude-code/src/tools/SkillTool/SkillTool.ts:619), then branches. A forked skill (context === 'fork') runs in an isolated sub-agent with its own token budget, inherited tools, model override, and effort, streaming progress back to the parent (claude-code/src/tools/SkillTool/SkillTool.ts:122-289). The default inline path calls processPromptSlashCommand, which is where the full markdown is finally read from disk (claude-code/src/tools/SkillTool/SkillTool.ts:635-643); the command's getPromptForCommand prepends the skill's base directory, substitutes $ARGUMENTS/${CLAUDE_SKILL_DIR}/${CLAUDE_SESSION_ID}, and — except for untrusted MCP skills — executes any inline shell commands before returning the expanded text (claude-code/src/skills/loadSkillsDir.ts:270-399). The expanded content is injected as newMessages, and a contextModifier chains the skill's allowed-tools, model override, and effort level onto the live context (claude-code/src/tools/SkillTool/SkillTool.ts:775-839). The loaded content is registered with the compaction-preservation state via addInvokedSkill, keyed by skill path, so a compaction event can re-inject it later (claude-code/src/tools/SkillTool/SkillTool.ts:1088-1093). Telemetry logs the execution context (inline/forked/remote) and source for every invocation (claude-code/src/tools/SkillTool/SkillTool.ts:152-203). Remote canonical skills (_canonical_<slug>, an ant-only experiment) bypass the local registry entirely and load their SKILL.md from AKI/GCS with caching (claude-code/src/tools/SkillTool/SkillTool.ts:605-612) — the rare herb that has no drawer on the wall and is fetched from afar only when ordered.
Why it matters
A full skill library can run to tens of thousands of tokens; pasting all of it into every turn would crowd out the user's actual work and inflate cache_creation on turn one. The two-phase split decouples how many skills exist from how many tokens they cost when idle. Discovery is bounded to 1% of the window (claude-code/src/tools/SkillTool/prompt.ts:20-23) and each entry hard-capped at 250 chars (claude-code/src/tools/SkillTool/prompt.ts:25-29), so adding skills grows the catalog without proportionally growing the standing context tax — full capability is materialized lazily, exactly once, at invocation (claude-code/src/tools/SkillTool/SkillTool.ts:635-643).
The truncation policy encodes a priority: Anthropic-curated bundled skills keep full descriptions even under budget pressure, and only the long tail degrades — first to trimmed descriptions, then to names-only (claude-code/src/tools/SkillTool/prompt.ts:92-114, claude-code/src/tools/SkillTool/prompt.ts:123-142). Per-agent tracking guarantees every subagent still gets a turn-0 catalog instead of silently inheriting an empty one (claude-code/src/utils/attachments.ts:2603-2607), while resume-suppression avoids paying for the same listing twice across process respawns (claude-code/src/utils/attachments.ts:2633-2635). And because invoked content is preserved in compaction state (claude-code/src/tools/SkillTool/SkillTool.ts:1088-1093), a skill that has done real work is not silently forgotten when history is squeezed. The net effect: the model sees a wide menu cheaply, commits tokens only to the one skill it chooses, and that choice survives the rest of the conversation.
Read the source
- The two-phase contract — listing for discovery, body loaded on invoke: claude-code/src/tools/SkillTool/prompt.ts:25-29, claude-code/src/tools/SkillTool/SkillTool.ts:635-643.
- The discovery budget — 1% of context (8K fallback) and the budget-aware formatter: claude-code/src/tools/SkillTool/prompt.ts:20-23, claude-code/src/tools/SkillTool/prompt.ts:70-171.
- Truncation policy — bundled never trimmed, names-only under extreme pressure: claude-code/src/tools/SkillTool/prompt.ts:92-114, claude-code/src/tools/SkillTool/prompt.ts:123-142.
- Candidate sourcing — local skills and MCP skills filters: claude-code/src/commands.ts:561-581, claude-code/src/commands.ts:547-559.
- Listing assembly — per-agent tracking, skill-search filtering, the
skill_listingattachment, and the SkillTool prompt: claude-code/src/utils/attachments.ts:2661-2751, claude-code/src/utils/attachments.ts:2603-2607, claude-code/src/utils/attachments.ts:2633-2635, claude-code/src/utils/attachments.ts:2638-2659, claude-code/src/utils/attachments.ts:2668-2673, claude-code/src/utils/attachments.ts:2685-2697, claude-code/src/tools/SkillTool/prompt.ts:173-196. - Invocation — validate, inline expansion, forked sub-agent, remote canonical, usage tracking, context modifiers, compaction preservation, telemetry: claude-code/src/tools/SkillTool/SkillTool.ts:354-430, claude-code/src/tools/SkillTool/SkillTool.ts:122-289, claude-code/src/tools/SkillTool/SkillTool.ts:605-612, claude-code/src/tools/SkillTool/SkillTool.ts:619, claude-code/src/tools/SkillTool/SkillTool.ts:775-839, claude-code/src/tools/SkillTool/SkillTool.ts:1088-1093, claude-code/src/tools/SkillTool/SkillTool.ts:152-203.
- Skill parsing and on-demand prompt building: claude-code/src/skills/loadSkillsDir.ts:185-265, claude-code/src/skills/loadSkillsDir.ts:270-399.
来源 · Source citations
- [1]
claude-code/src/tools/SkillTool/prompt.ts:20-23 - [2]
claude-code/src/tools/SkillTool/prompt.ts:25-29 - [3]
claude-code/src/tools/SkillTool/prompt.ts:70-171 - [4]
claude-code/src/tools/SkillTool/prompt.ts:92-114 - [5]
claude-code/src/tools/SkillTool/prompt.ts:123-142 - [6]
claude-code/src/tools/SkillTool/prompt.ts:173-196 - [7]
claude-code/src/utils/attachments.ts:2603-2607 - [8]
claude-code/src/utils/attachments.ts:2633-2635 - [9]
claude-code/src/utils/attachments.ts:2638-2659 - [10]
claude-code/src/utils/attachments.ts:2661-2751 - [11]
claude-code/src/utils/attachments.ts:2668-2673 - [12]
claude-code/src/utils/attachments.ts:2685-2697 - [13]
claude-code/src/tools/SkillTool/SkillTool.ts:122-289 - [14]
claude-code/src/tools/SkillTool/SkillTool.ts:152-203 - [15]
claude-code/src/tools/SkillTool/SkillTool.ts:354-430 - [16]
claude-code/src/tools/SkillTool/SkillTool.ts:605-612 - [17]
claude-code/src/tools/SkillTool/SkillTool.ts:619 - [18]
claude-code/src/tools/SkillTool/SkillTool.ts:635-643 - [19]
claude-code/src/tools/SkillTool/SkillTool.ts:775-839 - [20]
claude-code/src/tools/SkillTool/SkillTool.ts:1088-1093 - [21]
claude-code/src/commands.ts:547-559 - [22]
claude-code/src/commands.ts:561-581 - [23]
claude-code/src/skills/loadSkillsDir.ts:185-265 - [24]
claude-code/src/skills/loadSkillsDir.ts:270-399