A deep dive into the skills system in Kilo Code, explaining discovery, loading, activation, and best practices for writing skills that trigger reliably.
- Discovery vs Loading vs Activation
- What Goes in the System Prompt
- How Matching Works
- Mode-Specific Skills
- Override Priority
- UI Indicators
- Best Practices for Reliable Triggering
- Code References
Happens when Kilo Code starts, in SkillsManager.discoverSkills():
- Scans
.kilocode/skills/(project-level) and~/.kilocode/skills/(global) - Also scans mode-specific directories like
skills-code/,skills-architect/ - Parses SKILL.md frontmatter (only name + description)
- Stores metadata in memory (
Map<string, SkillMetadata>)
// From src/services/skills/SkillsManager.ts
async discoverSkills(): Promise<void> {
this.skills.clear()
const skillsDirs = await this.getSkillsDirectories()
for (const { dir, source, mode } of skillsDirs) {
await this.scanSkillsDirectory(dir, source, mode)
}
}Reading the full SKILL.md content happens only when needed via getSkillContent():
// From src/services/skills/SkillsManager.ts
async getSkillContent(name: string, currentMode?: string): Promise<SkillContent | null> {
// ... find skill ...
const fileContent = await fs.readFile(skill.path, "utf-8")
const { content: body } = matter(fileContent)
return {
...skill,
instructions: body.trim(),
}
}When the LLM decides to use a skill. The SKILL.md body is read at that moment via read_file tool.
Only name, description, and path - NOT the full SKILL.md content.
From src/core/prompts/sections/skills.ts:
<available_skills>
<skill>
<name>translation</name>
<description>Guidelines for translating and localizing...</description>
<location>/absolute/path/to/SKILL.md</location>
</skill>
</available_skills>
<mandatory_skill_check>
REQUIRED PRECONDITION
Before producing ANY user-facing response, you MUST perform a skill applicability check.
Step 1: Skill Evaluation
- Evaluate the user's request against ALL available skill <description> entries
- Determine whether at least one skill clearly and unambiguously applies
Step 2: Branching Decision
<if_skill_applies>
- Select EXACTLY ONE skill
- Prefer the most specific skill when multiple skills match
- Read the full SKILL.md file at the skill's <location>
- Load the SKILL.md contents fully into context BEFORE continuing
- Follow the SKILL.md instructions precisely
</if_skill_applies>
<if_no_skill_applies>
- Proceed with a normal response
- Do NOT load any SKILL.md files
</if_no_skill_applies>
</mandatory_skill_check>The LLM decides - there's no keyword matching, semantic search, or code-based logic.
The system prompt instructs the LLM to:
- Evaluate your request against ALL skill descriptions
- If a skill "clearly and unambiguously applies" → read the SKILL.md file
- If no skill applies → proceed normally
- Skill:
overnight-runwith description "autonomous overnight workflow execution" - Your message: "run this overnight autonomously"
- The LLM sees the skill list, evaluates descriptions, and decides whether to
read_filethe SKILL.md
Every response - the mandatory_skill_check is part of the system prompt sent with every request.
Skills in skills-code/ are filtered to only appear when in code mode:
// From src/services/skills/SkillsManager.ts
getSkillsForMode(currentMode: string): SkillMetadata[] {
const resolvedSkills = new Map<string, SkillMetadata>()
for (const skill of this.skills.values()) {
// Skip mode-specific skills that don't match current mode
if (skill.mode && skill.mode !== currentMode) continue
// ... override resolution ...
}
return Array.from(resolvedSkills.values())
}So skills-code/overnight-run/ would only show up in the system prompt when you're in code mode - it's not just hidden in UI, it's not sent to the LLM at all in other modes.
If you have the same skill name in multiple places:
- Project > Global - project-level skills override global ones
- Mode-specific > Generic -
skills-code/foo/overridesskills/foo/when in code mode
// From src/services/skills/SkillsManager.ts
private shouldOverrideSkill(existing: SkillMetadata, newSkill: SkillMetadata): boolean {
// Project always overrides global
if (newSkill.source === "project" && existing.source === "global") return true
if (newSkill.source === "global" && existing.source === "project") return false
// Same source: mode-specific overrides generic
if (newSkill.mode && !existing.mode) return true
if (!newSkill.mode && existing.mode) return false
return false
}The Settings → Skills tab shows:
- List of discovered skills (project + global)
- Their descriptions and mode restrictions
- Delete buttons
This is inventory management only - it doesn't track which skills were actually used.
No dedicated "skill activated" indicator. There's no badge, toast, or status bar showing "Skill X activated".
read_filetool call - The most visible sign. When the LLM uses a skill, it callsread_fileon the SKILL.md path. You'll see this in the chat.- LLM's response text - The LLM might mention "I'm using the translation skill" but this is model-dependent.
- Internal verification - The prompt includes
<skill_check_completed>true|false</skill_check_completed>for the LLM's internal reasoning, but this isn't parsed or displayed.
The description is what the LLM evaluates. Make it match how users phrase requests.
Good: "autonomous overnight workflow execution with progress monitoring" Bad: "overnight stuff"
Vague descriptions lead to uncertain matching.
Descriptions should describe WHEN to use the skill:
- "Guidelines for translating and localizing..."
- "Workflow for deploying to production..."
- "Process for reviewing pull requests..."
Saying "use the overnight-run skill" will definitely trigger it since the LLM sees the skill name in the list.
The LLM interprets, so "run overnight" might work but "schedule for tonight" might not. Test your skill with different phrasings.
Per the spec: 1-64 chars, lowercase letters/numbers/hyphens only, no leading/trailing hyphens, no consecutive hyphens.
| File | Purpose |
|---|---|
src/services/skills/SkillsManager.ts |
Discovery, loading, mode filtering |
src/core/prompts/sections/skills.ts |
System prompt generation |
src/shared/skills.ts |
Type definitions |
webview-ui/src/components/kilocode/settings/InstalledSkillsView.tsx |
UI for viewing installed skills |
---
name: my-skill
description: When to use this skill - this is what the LLM evaluates
---
# Instructions
The full content here is only loaded when the skill is activated.
This can be as long as needed - it's not in the system prompt by default.Based on Kilo Code source code analysis, January 2026