Skip to content

Claude Code Skills System -- Implementation Details ​

A deep dive into how Skills are discovered, loaded, injected, executed, and managed.

Architecture · Discovery & Loading · Frontmatter Parsing · Injection · Execution Engine · Fork Execution · Conditional Activation · Hook Integration · Permission System · Complete Lifecycle · Source Index

Skills Architecture Overview


1. Overall Architecture ​

The Skills system consists of 5 core modules working together:

┌─────────────────────────────────────────────────────┐
│                   Skills System                      │
│                                                     │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │  Discovery   │  │   Prompt     │  │  SkillTool │ │
│  │  & Loading   │→│  Injection   │→│  Execution  │ │
│  └─────────────┘  └──────────────┘  └────────────┘ │
│         ↑                                    ↓      │
│  ┌─────────────┐                     ┌────────────┐ │
│  │  Activation  │                     │  Context   │ │
│  │  Conditional │←────────────────────│  Modifier  │ │
│  └─────────────┘                     └────────────┘ │
└─────────────────────────────────────────────────────┘

Module Responsibilities ​

ModuleCore FileResponsibility
DiscoveryloadSkillsDir.tsDiscover and load Skills from 6 sources
Promptprompt.ts + attachments.tsInject Skill listing into system-reminder
SkillToolSkillTool.tsValidation, permission checks, Skill execution
ActivationloadSkillsDir.ts (second half)Conditional activation and dynamic discovery
ContextforkedAgent.tsContext preparation and modification

2. Skill Discovery and Loading ​

Loading Entry Point ​

Skills loading begins with the getSkills() function in commands.ts:

typescript
// src/commands.ts:351-396
async function getSkills(cwd: string) {
  const [skillDirCommands, pluginSkills] = await Promise.all([
    getSkillDirCommands(cwd),    // Directory Skills (managed/user/project)
    getPluginSkills(),            // Plugin Skills
  ])
  const bundledSkills = getBundledSkills()          // Built-in Skills
  const builtinPluginSkills = getBuiltinPluginSkillCommands()  // Built-in plugin Skills
  return { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills }
}

Aggregation and Ordering ​

All Skills from all sources are aggregated in loadAllCommands(), ordered by priority:

typescript
// src/commands.ts:447-467
const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
  return [
    ...bundledSkills,        // 1. Built-in Skills (highest priority)
    ...builtinPluginSkills,  // 2. Built-in plugin Skills
    ...skillDirCommands,     // 3. Directory Skills (managed → user → project)
    ...workflowCommands,     // 4. Workflow commands
    ...pluginCommands,       // 5. Plugin commands
    ...pluginSkills,         // 6. Plugin Skills
    ...COMMANDS(),           // 7. Built-in commands (lowest priority)
  ]
})

Key feature: Uses memoize for caching to avoid redundant disk I/O.

Directory Skill Loading Flow ​

Skill Loading Flow

getSkillDirCommands() is the core loading function for directory Skills:

getSkillDirCommands(cwd)
├─ Determine loading paths
│  ├─ managed: ${MANAGED_PATH}/.claude/skills/
│  ├─ user:    ~/.claude/skills/
│  ├─ project: .claude/skills/ (traverse upward to HOME)
│  └─ additional: paths specified via --add-dir
│
├─ Parallel loading (Promise.all)
│  ├─ loadSkillsFromSkillsDir(managedDir, 'policySettings')
│  ├─ loadSkillsFromSkillsDir(userDir, 'userSettings')
│  ├─ loadSkillsFromSkillsDir(projectDirs, 'projectSettings')
│  ├─ loadSkillsFromSkillsDir(additionalDirs, 'projectSettings')
│  └─ loadSkillsFromCommandsDir(cwd)  ← Legacy /commands/ format compatibility
│
├─ Deduplication (by realpath)
│  └─ getFileIdentity(filePath) → realpath resolves symlinks
│     └─ seenFileIds Map, first occurrence wins
│
└─ Separate conditional Skills
   ├─ No paths → unconditionalSkills (immediately available)
   └─ Has paths → conditionalSkills Map (awaiting activation)

Deduplication Mechanism ​

typescript
// src/skills/loadSkillsDir.ts:725-763
const fileIds = await Promise.all(
  allSkillsWithPaths.map(({ skill, filePath }) =>
    skill.type === 'prompt'
      ? getFileIdentity(filePath)  // realpath() resolves symlinks
      : Promise.resolve(null),
  ),
)

const seenFileIds = new Map<string, SettingSource>()
for (const entry of allSkillsWithPaths) {
  const fileId = fileIds[i]
  const existingSource = seenFileIds.get(fileId)
  if (existingSource !== undefined) continue  // Skip duplicates
  seenFileIds.set(fileId, skill.source)
  deduplicatedSkills.push(skill)
}

Bundled Skill Registration ​

Built-in Skills use an entirely different registration path:

typescript
// src/skills/bundledSkills.ts:53-100
export function registerBundledSkill(definition: BundledSkillDefinition): void {
  // If files are present, create extraction directory and lazy extraction logic
  if (files && Object.keys(files).length > 0) {
    skillRoot = getBundledSkillExtractDir(definition.name)
    // Extract files to disk on first invocation
    getPromptForCommand = async (args, ctx) => {
      extractionPromise ??= extractBundledSkillFiles(name, files)
      const extractedDir = await extractionPromise
      const blocks = await inner(args, ctx)
      return prependBaseDir(blocks, extractedDir)
    }
  }

  const command: Command = {
    type: 'prompt',
    source: 'bundled',
    loadedFrom: 'bundled',
    // ...other fields
  }
  bundledSkills.push(command)
}

File extraction: Bundled Skills can include a files: Record<string, string> field. On first invocation, these files are extracted to disk (getBundledSkillExtractDir()), allowing the model to access them via Read/Grep.

Startup Registration ​

typescript
// src/skills/bundled/index.ts:13-58
export function initBundledSkills(): void {
  require('./verify.js').registerVerifySkill()
  require('./debug.js').registerDebugSkill()
  require('./remember.js').registerRememberSkill()
  // ...
  if (feature('AGENT_TRIGGERS')) {
    require('./loop.js').registerLoopSkill()     // Feature-gated
  }
  if (feature('KAIROS')) {
    require('./dream.js').registerDreamSkill()    // Feature-gated
  }
}

Plugin Skill Loading ​

Plugin System
├─ loadAllPluginsCacheOnly()
│  └─ Get all enabled plugins
│
├─ For each plugin:
│  ├─ Read manifest.skillsPath → default Skills directory
│  ├─ Read manifest.skillsPaths[] → additional Skills directories
│  └─ loadSkillsFromDirectory() to load SKILL.md
│
├─ Namespacing:
│  └─ {pluginName}:{namespace}:{skillName}
│     e.g.: superpowers:code-reviewer
│
└─ Variable substitution:
   ├─ ${CLAUDE_PLUGIN_ROOT} → plugin root directory
   ├─ ${CLAUDE_PLUGIN_DATA} → plugin data directory
   ├─ ${CLAUDE_SKILL_DIR}   → skill directory
   └─ ${user_config.X}      → user configuration values

MCP Skill Loading ​

typescript
// src/services/mcp/client.ts:2030-2102
// MCP prompts are converted to Command objects
async function fetchCommandsForClient(client) {
  const prompts = await client.listPrompts()
  return prompts.map(prompt => ({
    type: 'prompt',
    name: `mcp__${normalizeNameForMCP(serverName)}__${prompt.name}`,
    source: 'mcp',
    loadedFrom: 'mcp',
    // getPromptForCommand calls the MCP server to fetch content
  }))
}

Feature gate: feature('MCP_SKILLS') controls whether MCP Skills are available.


3. Frontmatter Parsing ​

Parsing Flow ​

SKILL.md file
    ↓
parseFrontmatter()              ← frontmatterParser.ts
    ├─ Separate YAML frontmatter from Markdown content
    ├─ quoteProblematicValues()  ← Handle special characters (glob patterns, etc.)
    └─ parseYaml()               ← Parse YAML
    ↓
parseSkillFrontmatterFields()   ← loadSkillsDir.ts:185-265
    ├─ description extraction priority:
    │  1. frontmatter.description field
    │  2. First # heading in Markdown
    │  3. Skill name as fallback
    ├─ parseUserSpecifiedModel()     ← Model alias resolution
    ├─ parseEffortValue()            ← Effort level parsing
    ├─ parseHooksFromFrontmatter()   ← Hook configuration validation
    ├─ parseBooleanFrontmatter()     ← Boolean field parsing
    └─ parseSlashCommandToolsFromFrontmatter() ← Tool list parsing
    ↓
createSkillCommand()            ← loadSkillsDir.ts:270-401
    └─ Generate Command object (with getPromptForCommand closure)

FrontmatterData Type Definition ​

typescript
// src/utils/frontmatterParser.ts:10-59
export type FrontmatterData = {
  'allowed-tools'?: string | string[] | null
  description?: string | null
  'argument-hint'?: string | null
  when_to_use?: string | null
  version?: string | null
  model?: string | null          // haiku, sonnet, opus, inherit
  'user-invocable'?: string | null
  'disable-model-invocation'?: string | null
  hooks?: HooksSettings | null
  effort?: string | null         // low, medium, high, max, or numeric
  context?: 'inline' | 'fork' | null
  agent?: string | null
  paths?: string | string[] | null
  shell?: string | null          // bash, powershell
  [key: string]: unknown
}

The getPromptForCommand Closure ​

Each Command object contains a getPromptForCommand closure that executes upon invocation:

typescript
// src/skills/loadSkillsDir.ts:344-399
async getPromptForCommand(args, toolUseContext) {
  // 1. Add base directory header
  let finalContent = baseDir
    ? `Base directory for this skill: ${baseDir}\n\n${markdownContent}`
    : markdownContent

  // 2. Argument substitution
  finalContent = substituteArguments(finalContent, args, true, argumentNames)

  // 3. Skill directory variable substitution
  if (baseDir) {
    finalContent = finalContent.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir)
  }

  // 4. Session ID substitution
  finalContent = finalContent.replace(/\$\{CLAUDE_SESSION_ID\}/g, getSessionId())

  // 5. Execute inline shell commands (skipped for MCP Skills — security restriction)
  if (loadedFrom !== 'mcp') {
    finalContent = await executeShellCommandsInPrompt(finalContent, context)
  }

  return [{ type: 'text', text: finalContent }]
}

4. Skill Injection into Conversations ​

Injection Flow ​

Skill Listing Injection

Skills are injected into conversations via system-reminder messages:

Start of each conversation turn
    ↓
getSkillListingAttachments()          ← attachments.ts:2600-2747
    ├─ getSkillToolCommands(cwd)      ← Get all model-invocable Skills
    ├─ getMcpSkillCommands()          ← Get MCP Skills
    ├─ sentSkillNames Map tracking    ← Avoid duplicate sends (per-agent)
    └─ formatCommandsWithinBudget()   ← Truncate to context budget
    ↓
Returns Attachment:
  { type: 'skill_listing', content, skillCount, isInitial }
    ↓
normalizeAttachmentForAPI()           ← messages.ts:3732-3737
    ↓
Wrapped as <system-reminder> user message:
  "The following skills are available for use with the Skill tool:
   - commit: Create a git commit...
   - review-pr: Review a pull request...
   - superpowers:code-reviewer: Expert code review..."

Context Budget Control ​

typescript
// src/tools/SkillTool/prompt.ts:21-29
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01  // 1% of context window
export const CHARS_PER_TOKEN = 4
export const DEFAULT_CHAR_BUDGET = 8_000           // 200k × 4 × 1% fallback
export const MAX_LISTING_DESC_CHARS = 250          // Max characters per description

Truncation strategy:

formatCommandsWithinBudget(commands, contextWindowTokens)
    ├─ Calculate total budget = contextWindowTokens × 4 × 1%
    ├─ Try full descriptions
    │  └─ Total chars ≤ budget → output all
    │
    ├─ Partition: Bundled (never truncated) + rest
    │  ├─ Bundled Skills always retain full descriptions
    │  └─ Remaining Skills split the leftover budget evenly
    │
    ├─ Truncate descriptions → maxDescLen characters
    │  └─ maxDescLen < 20 → extreme case: non-Bundled show name only
    │
    └─ Output format: "- skill-name: description..."

SkillTool Prompt ​

The tool prompt definition seen by the model:

typescript
// src/tools/SkillTool/prompt.ts:173-196
export const getPrompt = memoize(async (_cwd: string): Promise<string> => {
  return `Execute a skill within the main conversation

When users ask you to perform tasks, check if any of the available skills match.

How to invoke:
- Use this tool with the skill name and optional arguments
- Examples:
  - skill: "pdf"
  - skill: "commit", args: "-m 'Fix bug'"

Important:
- Available skills are listed in system-reminder messages
- When a skill matches, invoke BEFORE generating any other response
- NEVER mention a skill without calling this tool
- Do not invoke a skill that is already running
`
})

5. SkillTool Execution Engine ​

Tool Definition ​

typescript
// src/tools/SkillTool/SkillTool.ts:331-340
export const SkillTool = buildTool({
  name: 'Skill',
  inputSchema: z.object({
    skill: z.string(),    // Skill name
    args: z.string().optional(),  // Optional arguments
  }),
  outputSchema: z.union([inlineOutputSchema, forkedOutputSchema]),
})

Execution Flow ​

SkillTool Execution Flow

SkillTool.call({ skill, args })
    │
    ├─ 1. Normalize input
    │  └─ Strip leading /, trim whitespace
    │
    ├─ 2. Remote Skill check (experimental)
    │  └─ _canonical_<slug> prefix → executeRemoteSkill()
    │
    ├─ 3. Find Command object
    │  └─ getAllCommands(context) → findCommand(name, commands)
    │
    ├─ 4. Record usage frequency
    │  └─ recordSkillUsage(commandName) → influences sorting recommendations
    │
    ├─ 5. Determine execution path
    │  ├─ command.context === 'fork'
    │  │  └─ → executeForkedSkill()  [see Section 6]
    │  │
    │  └─ Default: inline
    │     ├─ processPromptSlashCommand()
    │     │  └─ getMessagesForPromptSlashCommand()
    │     │     ├─ command.getPromptForCommand(args, context)
    │     │     ├─ registerSkillHooks()      ← Register hooks
    │     │     ├─ addInvokedSkill()         ← Record (restored after compression)
    │     │     ├─ formatCommandLoadingMetadata()
    │     │     │  └─ <command-name>/skillName</command-name>
    │     │     └─ Extract attachments → create messages
    │     │
    │     ├─ Extract metadata: allowedTools, model, effort
    │     ├─ tagMessagesWithToolUseID() ← Associate with tool_use
    │     └─ Return { newMessages, contextModifier }
    │
    └─ 6. contextModifier() closure
       ├─ Update allowedTools
       │  └─ appState.toolPermissionContext.alwaysAllowRules.command
       ├─ Update model
       │  └─ resolveSkillModelOverride() preserves [1m] suffix
       └─ Update effort
          └─ appState.effortValue

Validation Logic ​

typescript
// src/tools/SkillTool/SkillTool.ts:354-430
async validateInput({ skill }, context) {
  // 1. Format check — non-empty
  // 2. Normalize — strip leading /
  // 3. Remote Skill check — _canonical_ prefix
  // 4. Lookup — findCommand() within getAllCommands()
  // 5. Disabled check — disableModelInvocation
  // 6. Type check — must be 'prompt' type
}

Error code definitions:

errorCodeMeaning
1Invalid format (empty skill name)
2Unknown skill
4Model invocation disabled
5Not a prompt type
6Remote skill not found

getAllCommands -- MCP Skill Merging ​

typescript
// src/tools/SkillTool/SkillTool.ts:81-94
async function getAllCommands(context: ToolUseContext): Promise<Command[]> {
  // Get MCP Skills from AppState (loadedFrom === 'mcp')
  const mcpSkills = context.getAppState()
    .mcp.commands.filter(
      cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp',
    )
  if (mcpSkills.length === 0) return getCommands(getProjectRoot())
  const localCommands = await getCommands(getProjectRoot())
  return uniqBy([...localCommands, ...mcpSkills], 'name')
}

6. Fork Sub-Agent Execution ​

Execution Flow ​

executeForkedSkill(command, commandName, args, context, ...)
    │
    ├─ 1. Create sub-agent ID
    │  └─ agentId = createAgentId()
    │
    ├─ 2. Telemetry recording
    │  └─ logEvent('tengu_skill_tool_invocation', { execution_context: 'fork' })
    │
    ├─ 3. Prepare fork context
    │  └─ prepareForkedCommandContext(command, args, context)
    │     ├─ command.getPromptForCommand(args, context)  ← Get skill content
    │     ├─ parseToolListFromCLI(allowedTools)           ← Parse tool whitelist
    │     ├─ createGetAppStateWithAllowedTools()          ← Modify AppState
    │     ├─ Select agent: command.agent ?? 'general-purpose'
    │     └─ promptMessages = [createUserMessage(skillContent)]
    │
    ├─ 4. Merge effort
    │  └─ command.effort → inject into agentDefinition
    │
    ├─ 5. Run sub-agent
    │  └─ for await (message of runAgent({
    │       agentDefinition,
    │       promptMessages,
    │       toolUseContext: { ...context, getAppState: modifiedGetAppState },
    │       model: command.model,
    │       override: { agentId },
    │     }))
    │     └─ Collect messages + report progress (onProgress)
    │
    ├─ 6. Extract results
    │  └─ extractResultText(agentMessages)
    │     └─ Get text from the last assistant message
    │
    └─ 7. Cleanup
       └─ clearInvokedSkillsForAgent(agentId)

prepareForkedCommandContext ​

typescript
// src/utils/forkedAgent.ts:191-232
export async function prepareForkedCommandContext(
  command: PromptCommand,
  args: string,
  context: ToolUseContext,
): Promise<PreparedForkedContext> {
  // Get skill content (with argument substitution and shell execution)
  const skillPrompt = await command.getPromptForCommand(args, context)
  const skillContent = skillPrompt.map(b => b.type === 'text' ? b.text : '').join('\n')

  // Build tool whitelist
  const allowedTools = parseToolListFromCLI(command.allowedTools ?? [])
  const modifiedGetAppState = createGetAppStateWithAllowedTools(
    context.getAppState, allowedTools,
  )

  // Select agent type
  const agentTypeName = command.agent ?? 'general-purpose'
  const baseAgent = agents.find(a => a.agentType === agentTypeName)

  // Build prompt messages
  const promptMessages = [createUserMessage({ content: skillContent })]

  return { skillContent, modifiedGetAppState, baseAgent, promptMessages }
}

Inline vs Fork Return Differences ​

Inline return:

typescript
{
  data: {
    success: true,
    commandName: 'commit',
    allowedTools: ['Bash', 'Read'],
    model: 'sonnet',
    status: 'inline',
  },
  newMessages: [...],        // Injected into conversation
  contextModifier: (ctx) => { ... },  // Modifies context
}

Fork return:

typescript
{
  data: {
    success: true,
    commandName: 'verify',
    status: 'forked',
    agentId: 'agent_abc123',
    result: 'Verification passed, all tests have run...',
  },
  // No newMessages — result is embedded in the tool_result block
}

7. Conditional Activation and Dynamic Discovery ​

Conditional Skills ​

Conditional Activation Mechanism

Skills with paths frontmatter are not immediately exposed to the model:

At startup
├─ Load all Skills
├─ Those with paths → conditionalSkills Map
└─ Those without paths → immediately available

At runtime (triggered by file operations)
├─ activateConditionalSkillsForPaths(filePaths, cwd)
│  ├─ Iterate over conditionalSkills Map
│  ├─ Match paths patterns using the ignore library
│  │  └─ filePath converted to cwd-relative path before matching
│  ├─ On match:
│  │  ├─ Move to dynamicSkills Map
│  │  ├─ Remove from conditionalSkills
│  │  ├─ Add to activatedConditionalSkillNames Set
│  │  └─ Log telemetry: tengu_dynamic_skills_changed
│  └─ Once activated, remains active for the session
└─ Notify cache invalidation → skillsLoaded.emit()

Dynamic Discovery ​

When operating on files in deeply nested directories, the system automatically discovers new Skills:

typescript
// src/skills/loadSkillsDir.ts:861-915
export async function discoverSkillDirsForPaths(
  filePaths: string[],
  cwd: string,
): Promise<string[]> {
  for (const filePath of filePaths) {
    let currentDir = dirname(filePath)
    // Traverse upward from the file's directory to cwd (excluding cwd itself)
    while (currentDir.startsWith(resolvedCwd + pathSep)) {
      const skillDir = join(currentDir, '.claude', 'skills')
      if (!dynamicSkillDirs.has(skillDir)) {
        dynamicSkillDirs.add(skillDir)
        await fs.stat(skillDir)  // Check if it exists
        // Check if ignored by .gitignore
        if (await isPathGitignored(currentDir, resolvedCwd)) continue
        newDirs.push(skillDir)
      }
      currentDir = dirname(currentDir)
    }
  }
  // Sort by depth (deepest first), ensuring nearest Skills have higher priority
  return newDirs.sort((a, b) => b.split(pathSep).length - a.split(pathSep).length)
}

Cache Invalidation Chain ​

Dynamic Skill change
    ↓
skillsLoaded.emit()
    ↓
clearCommandMemoizationCaches()
    ├─ loadAllCommands.cache.clear()
    ├─ getSkillToolCommands.cache.clear()
    ├─ getSlashCommandToolSkills.cache.clear()
    └─ clearSkillIndexCache()
    ↓
Next conversation turn loads the updated Skill list

8. Hook Integration ​

Hook Registration ​

Skills can declare hooks via frontmatter, which are automatically registered as session-level hooks upon invocation:

typescript
// src/utils/hooks/registerSkillHooks.ts:20-64
export function registerSkillHooks(
  setAppState, sessionId, hooks, skillName, skillRoot?,
): void {
  for (const eventName of HOOK_EVENTS) {
    const matchers = hooks[eventName]
    if (!matchers) continue
    for (const matcher of matchers) {
      for (const hook of matcher.hooks) {
        // once: true → automatically removed after one execution
        const onHookSuccess = hook.once
          ? () => removeSessionHook(setAppState, sessionId, eventName, hook)
          : undefined

        addSessionHook(
          setAppState, sessionId, eventName,
          matcher.matcher || '',
          hook, onHookSuccess, skillRoot,
        )
      }
    }
  }
}

Hook Lifecycle ​

Skill invocation
    ↓
processPromptSlashCommand()
    ├─ Check command.hooks
    └─ registerSkillHooks(setAppState, sessionId, hooks, skillName, skillRoot)
         ├─ Iterate over HOOK_EVENTS (PreToolUse, PostToolUse, Stop, ...)
         ├─ Register addSessionHook() for each matcher
         ├─ skillRoot → CLAUDE_PLUGIN_ROOT environment variable
         └─ once: true → removeSessionHook() after first execution
    ↓
During session
    ├─ Tool calls trigger PreToolUse/PostToolUse
    ├─ Matcher matches → execute hook command
    └─ Hooks with once: true are automatically removed after first execution

9. Permission System ​

Check Flow ​

checkPermissions({ skill, args }, context)
    │
    ├─ 1. Deny rule check (highest priority)
    │  └─ getRuleByContentsForTool(context, SkillTool, 'deny')
    │     ├─ Exact match: "commit" === commandName
    │     └─ Prefix match: "review:*" → commandName.startsWith("review")
    │
    ├─ 2. Remote Skill auto-allow
    │  └─ _canonical_<slug> → auto-allow (Ant-specific experimental)
    │
    ├─ 3. Allow rule check
    │  └─ getRuleByContentsForTool(context, SkillTool, 'allow')
    │
    ├─ 4. Safe property auto-allow
    │  └─ skillHasOnlySafeProperties(command)
    │     └─ SAFE_SKILL_PROPERTIES whitelist check
    │
    └─ 5. Default: ask user
       └─ Provide suggestions: exact allow + prefix allow

Safe Property Whitelist ​

If a Skill contains only the following properties (no hooks, no allowedTools, no fork), it is automatically allowed:

SAFE_SKILL_PROPERTIES = {
  type, name, description, contentLength, source,
  loadedFrom, progressMessage, userInvocable,
  disableModelInvocation, hasUserSpecifiedDescription,
  getPromptForCommand, userFacingName, ...
}

Core principle: Newly added frontmatter fields require permission by default, unless explicitly added to the whitelist.


10. Complete Lifecycle ​

Data Flow Overview ​

Complete Lifecycle

Phase 1: Discovery and Registration
──────────────────────────────────
CLI startup
    ├─ initBundledSkills()        → bundledSkills[]
    ├─ getPluginSkills()          → pluginSkills[]
    ├─ getSkillDirCommands(cwd)   → skillDirCommands[]
    │  └─ Conditional Skills → conditionalSkills Map
    └─ loadAllCommands()          → aggregate & memoize

Phase 2: Injection into Conversation
────────────────────────────────────
Each conversation turn
    ├─ getSkillListingAttachments()
    │  ├─ getSkillToolCommands()  → filter model-invocable Skills
    │  ├─ getMcpSkillCommands()   → MCP Skills
    │  └─ formatCommandsWithinBudget()  → truncate to budget
    └─ Wrap as <system-reminder> message and inject

Phase 3: Invocation and Execution
─────────────────────────────────
Model/user trigger
    ├─ SkillTool.validateInput()  → validation
    ├─ SkillTool.checkPermissions()  → permissions
    └─ SkillTool.call()
       ├─ Inline → processPromptSlashCommand()
       │  ├─ getPromptForCommand()  → content expansion
       │  ├─ registerSkillHooks()   → hook registration
       │  ├─ addInvokedSkill()      → record (restored after compression)
       │  └─ contextModifier()      → update tools/model/effort
       └─ Fork → executeForkedSkill()
          ├─ prepareForkedCommandContext()
          ├─ runAgent()             → sub-agent execution
          └─ extractResultText()    → extract results

Phase 4: Runtime Discovery
─────────────────────────
File operation trigger
    ├─ discoverSkillDirsForPaths()   → new Skills directories
    ├─ addSkillDirectories()          → load & register
    ├─ activateConditionalSkillsForPaths()  → conditional activation
    └─ clearCommandMemoizationCaches()      → cache invalidation

Skill Content Persistence ​

Inline Skill content is recorded to session state via addInvokedSkill(), ensuring it can be restored after context compression:

addInvokedSkill(name, path, content, agentId)
    ↓
Stored in session state
    ↓
On compression → buildPostCompactMessages()
    ↓
Restored scoped by agentId (prevents cross-agent leakage)

11. Source Code Index ​

Core Files ​

FileResponsibility
src/tools/SkillTool/SkillTool.tsSkillTool definition, validation, permissions, execution
src/tools/SkillTool/prompt.tsTool prompt, Skill listing formatting, budget control
src/skills/loadSkillsDir.tsDirectory Skill discovery, loading, deduplication, conditional activation
src/skills/bundledSkills.tsBuilt-in Skill registration system
src/skills/bundled/index.tsBuilt-in Skills initialization entry point
src/commands.tsCommand aggregation, ordering, filtering, cache management

Type Definitions ​

FileKey Types
src/types/command.tsPromptCommand, Command, LocalCommandResult
src/utils/frontmatterParser.tsFrontmatterData, ParsedMarkdown
src/skills/bundledSkills.tsBundledSkillDefinition

Utility Modules ​

FileResponsibility
src/utils/forkedAgent.tsFork context preparation, result extraction
src/utils/hooks/registerSkillHooks.tsSkill hook registration
src/utils/argumentSubstitution.tsArgument substitution ($ARGUMENTS, ${ARG1})
src/utils/promptShellExecution.tsInline shell command execution
src/utils/attachments.tsSkill listing attachment generation
src/utils/messages.tssystem-reminder wrapping
src/utils/plugins/loadPluginCommands.tsPlugin Skill loading
src/services/mcp/client.tsMCP Skill conversion
src/skills/mcpSkillBuilders.tsMCP Skill builder registration

Key Constants ​

ConstantValueLocation
SKILL_BUDGET_CONTEXT_PERCENT0.01prompt.ts:21
DEFAULT_CHAR_BUDGET8,000prompt.ts:23
MAX_LISTING_DESC_CHARS250prompt.ts:29
SKILL_TOOL_NAME'Skill'constants.ts

Telemetry Events ​

EventDescription
tengu_skill_tool_invocationSkill invocation (includes execution_context, invocation_trigger)
tengu_skill_tool_slash_prefixModel used the / prefix
tengu_dynamic_skills_changedDynamic Skills changed (conditional activation/directory discovery)
tengu_skill_descriptions_truncatedSkill descriptions were truncated

Released under the MIT License.