Skip to content

Skills system

The Skills system is the runtime that turns a markdown file + frontmatter into a model-invokable tool. Built-in Claude Code skills (claude-api), user skills (.claude/skills/), plugin-provided skills, and the SDK-registered skills all flow through the same parseSkillFrontmatterFields → createSkillCommand → processSkillMarkdown → getPromptForCommand pipeline.

Use this page alongside:

Source anchors

Semantic aliasString or symbolMeaning
SkillFrontmatterParserparseSkillFrontmatterFields(frontmatter, baseDir, name, kind="Skill")Parses a SKILL.md / command frontmatter block into a typed descriptor.
SkillCommandFactorycreateSkillCommand({skillName, displayName, description, markdownContent, allowedTools, argumentHint, argumentNames, whenToUse, version, model, disableModelInvocation, userInvocable, hooks, executionContext, agent, paths, effort, shell, createdBy, declaredFields, fallback, ...})Builds the in-memory command record returned to the runtime.
SkillMarkdownProcessorfunction processSkillMarkdown(markdown, vars)Strips HTML comments and substitutes {{var}} placeholders.
SkillBudgetFormatterfunction formatCommandsWithinBudget(commands, sessionContext, weighter, options)Tail-truncates skill descriptions to fit a token budget.
SkillToolInfogetSkillToolInfo(state), getSkillInfo(state), getLimitedSkillToolCommands(state)Surface for the Skill tool to ask “what skills are available and which apply?”.
ClaudeApiSkillRegistrarfunction registerClaudeApiSkill()Registers the built-in claude-api skill that ships with the bundle.
SkillSubcommandMatcherfunction matchSubcommand(input)Resolves the first whitespace-delimited token against the skill’s declared subcommands.
SkillStateRestorerfunction restoreSkillStateFromMessages(messages)Restores live skill state from a resumed transcript so a session reload knows which skills already ran.
SlashCommandDispatcherfunction processSlashCommand(input, ...), function processPromptSlashCommand(input, ...), function looksLikeCommand(text)Slash-command parser/dispatcher that resolves /skillname args into a skill invocation.
SkillLoadingMetadatafunction formatSkillLoadingMetadata(skill, phase="loading")Renders the user-facing skill-loading banner.
UserPromptExpansionRunnerasync function runUserPromptExpansionHook(...)Runs the UserPromptExpansion hook chain after slash-command resolution.
PromptCachefunction clearPromptCache()Clears the cached SDK skill prompt (re-fetched lazily by getPrompt).
HostedListSkillsListSkillsLists enabled claude.ai skills, optionally filtered by keyword.
HostedSearchSkillsSearchSkillsSearches the hosted organization/account skill catalog by intent keywords.
HostedSuggestSkillsSuggestSkillsFinds not-yet-enabled skills and renders an add card.
SkillProposalToolpropose_skills, tengu_propose_skillsPresents up to three complete new/improved SKILL.md drafts for host review.
BundledSkillWrapperLuWraps bundled commands that carry embedded files and defers extraction until invocation.
BundledSkillExtractorklt, Zry, vzu, rny, nny, getBundledSkillsRootMaterializes contained, exclusively-created files beneath the bundled-skills root.
CommandBudgetConstantsconst ww6 = 20 (minimum-per-skill chars), const Yz_ = 200000 (default token budget)Budget thresholds used by formatCommandsWithinBudget.

Bundle modules in cli.renamed.js

Semantic aliasLoader line(s)Representative renamed exportsAtlas entry
SkillsRuntime278242, 487278, 718869, 424568, 400592parseSkillFrontmatterFields, createSkillCommand, processSkillMarkdown, formatCommandsWithinBudget, getSkillToolInfo, getSkillInfo, getLimitedSkillToolCommands, registerClaudeApiSkill, matchSubcommand, restoreSkillStateFromMessages, processSlashCommand, processPromptSlashCommand, looksLikeCommand, runUserPromptExpansionHook, formatSkillLoadingMetadata, clearPromptCache, CLAUDE_API_SKILL_DESCRIPTIONBundle module map — integrations (MCP, plugins, MCPB, LSP)

Skill lifecycle

flowchart TD
Discovery[Disk scan / SDK register / plugin load] --> Frontmatter[parseSkillFrontmatterFields]
Frontmatter --> Validate{required fields ok?}
Validate -->|no| Skip[skip with debug warning]
Validate -->|yes| Build[createSkillCommand]
Build --> Registry[in-memory command registry]
Registry --> ToolInfo[Skill tool surfaces: name + description]
ToolInfo --> Budget[formatCommandsWithinBudget tail-truncates]
Budget --> Prompt[System prompt receives the command list]
Prompt --> User{model invokes Skill}
User --> Resolve[matchSubcommand resolves /name → skill]
Resolve --> Hook[runUserPromptExpansionHook can mutate input]
Hook --> Run[skill.getPromptForCommand expands markdown]
Run --> Substitute[processSkillMarkdown applies {{var}}]
Substitute --> Send[expanded prompt sent to model loop]

Frontmatter contract

parseSkillFrontmatterFields reads a YAML frontmatter object and returns a typed descriptor. The relevant fields:

FieldEffect
nameDisplayed name (defaults to the file/directory slug).
descriptionDescription shown to the model. Validated through bE(...); falls back to cHH(baseDir, kind) ("Skill" or "Prompt").
user-invocableBoolean — when false the skill is hidden from /help and slash autocomplete (set via jBH(...)).
modelOptional override resolved through parseUserSpecifiedModel(...). "inherit" means “use main-loop model” (returns undefined).
effortReasoning-effort override; parsed via JC(value); invalid values are logged and ignored.
allowed-toolsList of tools the skill may run; passed through at(...). Inside the skill body, ${CLAUDE_SKILL_DIR} placeholders are substituted with the skill’s base directory before the list is enforced.
argument-hintFree-text hint shown in the slash command line.
argumentsNamed arguments; parsed via O$8(...).
when_to_useOptional hint shown to the model.
versionSkill version string.
disable-model-invocationWhen true the skill is not exposed to the model at all (only invoked by user).
hooksPer-skill hook map (parsed via Y15(...)). Skills can register their own SessionStart/PreToolUse/etc. hooks.
context: "fork"Skills that need a forked subagent context (the only allowed value).
agentPin invocation to a specific agent (e.g. a subagent template).
shellShell override for slash-style skills (parsed via Yr$(...)).
created_by / improved_byWhen equal to "dream-proposal", attribution is preserved.
pathsRestrict the skill to specific path globs.
fallbackHook-style fallback resolution (GZH(...)).

parseSkillFrontmatterFields also captures declaredFields (the set of frontmatter keys actually present) via zr$(...), so later layers can warn on unknown fields without losing the original keys.

Tool-name allowlist substitution

createSkillCommand rewrites every ${CLAUDE_SKILL_DIR} placeholder in allowedTools to the skill’s base directory before building the command record. The same substitution happens to the markdown body inside getPromptForCommand. This is why a skill can ship allowedTools: ["Read(${CLAUDE_SKILL_DIR}/data/*)"] and have it scoped correctly per install location.

${CLAUDE_SESSION_ID} is also substituted in the body (using getSessionId()), and ${CLAUDE_EFFORT} is substituted using the skill’s effort value or the calling session’s getEffortValue().

Command record shape (createSkillCommand)

The returned record matches the in-memory slash-command interface and carries:

  • type: "prompt"
  • name, description, argumentHint, argNames, whenToUse, version, model, disableModelInvocation, userInvocable
  • context (only set when "fork")
  • agent, effort, paths, declaredFields, contentLength (markdown length)
  • isHidden = !userInvocable
  • progressMessage = "running"
  • userFacingName() — returns displayName || name
  • source, loadedFrom — provenance (settings / plugin / skill root / mcp / etc.)
  • createdBy, fallback
  • hooks — per-skill hook overrides
  • skillRoot — directory used for relative path resolution
  • getPromptForCommand(argv, sessionContext) — async function that expands the markdown body, substitutes variables, applies the per-skill allowedTools scope to the session, and returns [{type: "text", text: ...}]

getPromptForCommand expansion order

  1. If skillRoot is set, the body is prefixed with Base directory for this skill: <root>.
  2. BFH(...) injects positional/named arguments according to argNames.
  3. ${CLAUDE_SKILL_DIR} is substituted (only when skillRoot is set).
  4. ${CLAUDE_SESSION_ID} is substituted with the current session id.
  5. ${CLAUDE_EFFORT} is substituted with aT(model, effort).
  6. If the load source is “mcp”-like (A15(...)) and EM8() returns true, the body is rewritten through yM8(...) (MCP elicitation flavor).
  7. Otherwise dHH(...) runs general prompt expansion using a temporary toolPermissionContext whose alwaysAllowRules.command is merged with the skill’s allowedTools — the skill gets to run its declared tools without escalating permission for the rest of the session.

Markdown processing helpers

processSkillMarkdown(markdown, vars) does two passes:

  1. Strip all <!--...--> HTML comments (looping until no change) so users can leave triage notes in the SKILL.md without leaking them into the prompt.
  2. Substitute every {{var}} placeholder using the supplied vars map.

The Ce4/Se4/wKA helpers in the claude-api skill use this to materialize per-language documentation blocks. Se4(paths, files, vars) builds <doc path="...">...</doc> envelopes; wKA assembles the reading-guide-aware prompt for the detected project language.

Built-in claude-api skill

registerClaudeApiSkill() is the single registration call for Claude Code’s built-in API-help skill. It:

  • Registers a skill named "claude-api" with allowedTools: ["Read", "Grep", "Glob", "WebFetch"] and userInvocable: true.
  • Declares files: zKA() — the embedded markdown files keyed by language and topic.
  • Defines getPromptForCommand(argv):
    1. Calls fKA() to detect the project’s primary language by inspecting common files (requirements.txt/pyproject.toml → python, tsconfig.json/package.json → typescript, pom.xml → java, go.mod → go, Gemfile → ruby, *.cs/.csproj → csharp, composer.json → php, fallback curl).
    2. Emits tengu_claude_api_skill_loaded telemetry with the detected language, the subcommand resolved by matchSubcommand, and whether the user supplied arguments.
    3. Returns the assembled prompt from wKA(detectedLang, argv, vars) which includes a reading guide (“how to map a task to a doc file”) plus the language-specific documentation blocks and any user request text.

matchSubcommand(text) allows the user to type /claude-api migrate or /claude-api managed-agents-onboard; anything else resolves to "none".

The skill’s hard-coded description (CLAUDE_API_SKILL_DESCRIPTION) embeds an explicit trigger list (“when: code imports anthropic/@anthropic-ai/sdk; user asks for the Claude API…”) and a skip list (“openai/other-provider SDK, provider-neutral code”) so the model invokes it only when it actually fits.

Lazy extraction of bundled Skill files

Bundled commands can carry a files object (or an async function returning one) in addition to their prompt. Lu does not write those files at registration time. Instead, it wraps getPromptForCommand with a memoized first-invocation extraction:

  1. klt(skillName) selects a directory below getBundledSkillsRoot().
  2. On the first prompt request, the wrapper resolves files and calls Zry; concurrent/later requests share the same stored promise for that command record.
  3. vzu groups files by parent directory, creates those directories recursively with mode 0700, and writes files through rny.
  4. On success, the wrapper prefixes the prompt with Base directory for this skill: <path>. On extraction failure, Zry logs/telemeters the failure and the wrapper returns the original prompt without that extraction-directory prefix.

The extraction boundary has several source-visible containment rules (cli.renamed.js:421026-421174):

  • nny normalizes each embedded relative path and rejects absolute paths or any .. segment before joining it beneath the skill directory.
  • rny opens each file with write/create/exclusive flags and mode 0600; the first extraction therefore does not silently overwrite an existing path.
  • The open flags include O_NOFOLLOW when fs.constants.O_NOFOLLOW exists. The code uses O_NOFOLLOW ?? 0, so no-follow enforcement is platform-dependent and must not be described as universal.
  • The separate additional-file path (mhs) may tolerate EEXIST; that does not change the first extraction’s exclusive-create behavior.

This mechanism applies to bundled embedded assets, not ordinary filesystem-discovered SKILL.md directories, which already have a real skillRoot.

Budget-aware presentation

formatCommandsWithinBudget(commands, sessionContext, weighter, options) is the function that decides which skill descriptions get fully shown to the model:

  1. Each command becomes {cmd, full} where full is either - name: <description> or - name for name-only commands.
  2. If the total bytes fit the budget, every full description is emitted.
  3. Otherwise, bundled prompts and name-only commands are always kept; everything else is recomputed with a per-skill char allowance.
  4. When the caller supplies a weighter(command) (used to express “this skill is more relevant to the current turn”), high-weight commands get to keep their full description; low-weight ones drop to name-only.
  5. Names always survive — descriptions are the variable. The minimum per-skill description allowance is ww6 = 20 chars; below that the function falls back to name-only for all variable rows.
  6. When per-skill truncation kicks in, descriptions are truncated to the allowance with an ellipsis.

This is what the Skill tool calls to keep the system-prompt skill list under budget without dropping skills entirely.

Slash-command dispatch

looksLikeCommand(text) is the cheap predicate the input pipeline uses to short-circuit non-command input. processSlashCommand(...) walks the input, resolves it against the live command registry, runs runUserPromptExpansionHook(...) to let hooks rewrite the input, and ultimately calls the resolved command’s getPromptForCommand(...). processPromptSlashCommand(...) is the variant invoked when a slash command is found embedded inside a longer prompt (the user typed “do X and then /lint”).

Session restore

restoreSkillStateFromMessages(messages) walks a resumed transcript and re-derives which skills already loaded their content in the live state. This is how /resume brings back a session that previously loaded a skill — the skill’s <skill-loaded> tag in the transcript signals “already loaded” so the runtime doesn’t re-load it on resume.

Hosted skill discovery and proposals

The local SKILL.md runtime above is complemented by first-party hosted discovery descriptors. These descriptors use organization APIs and are visible only when isConnectorSuggestToolEnabled() passes—CLAUDE_CODE_REMOTE plus the first-party provider—so their presence in the bundle is not evidence that every local CLI session has them.

ToolInput/output roleImportant boundary
ListSkillsOptional keyword filter over enabled claude.ai skills; returns IDs, names, descriptions, and enabled state.Use this for capabilities the account already has. It calls the hosted list API, not the local skill directory scanner.
SearchSkillsOne to eight intent keywords; returns ranked hosted catalog records.Discovery only. A matching record is not automatically loaded or enabled.
SuggestSkillsTask keywords plus optional context label/trigger; searches and filters out already-enabled records, then returns a renderable card payload.The user adds the skill out of band. A later ListSkills call is the source of truth for whether it became enabled.
propose_skillsOne to three proposals containing slug, new/improvement kind, description, evidence paths, and a complete SKILL.md draft.Separate host review surface, not direct filesystem creation. Requires the remote-environment shape, CLAUDE_CODE_SYNC_SKILLS, tengu_propose_skills, and an additional host predicate.

Plugin discovery mirrors this sequence with ListPlugins, SearchPlugins, and SuggestPluginInstall; connector discovery uses SearchMcpRegistry → SuggestConnectors. See MCP, plugins, and hooks for the shared hosted boundary.

/team-onboarding is another built-in skill-shaped workflow, but it is deliberately user-only (disableModelInvocation: true) because it scans recent local session usage before drafting ONBOARDING.md. Its scan and optional hosted share lifecycle are documented in Team onboarding and share flows.

Created and maintained by Yingting Huang.