Skip to content

Instantly share code, notes, and snippets.

@markijbema
Created January 22, 2026 12:48
Show Gist options
  • Select an option

  • Save markijbema/bb9b5b47d862adf896c2b5d052481134 to your computer and use it in GitHub Desktop.

Select an option

Save markijbema/bb9b5b47d862adf896c2b5d052481134 to your computer and use it in GitHub Desktop.
How Skills Work in Kilo Code - Complete Technical Explanation

How Skills Work in Kilo Code

A deep dive into the skills system in Kilo Code, explaining discovery, loading, activation, and best practices for writing skills that trigger reliably.

Table of Contents


Discovery vs Loading vs Activation

Discovery (Startup)

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)
    }
}

Loading (On-Demand)

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(),
    }
}

Activation

When the LLM decides to use a skill. The SKILL.md body is read at that moment via read_file tool.


What Goes in the System Prompt

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>

How Matching Works

The LLM decides - there's no keyword matching, semantic search, or code-based logic.

The system prompt instructs the LLM to:

  1. Evaluate your request against ALL skill descriptions
  2. If a skill "clearly and unambiguously applies" → read the SKILL.md file
  3. If no skill applies → proceed normally

Example

  • Skill: overnight-run with description "autonomous overnight workflow execution"
  • Your message: "run this overnight autonomously"
  • The LLM sees the skill list, evaluates descriptions, and decides whether to read_file the SKILL.md

When is it checked?

Every response - the mandatory_skill_check is part of the system prompt sent with every request.


Mode-Specific Skills

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.


Override Priority

If you have the same skill name in multiple places:

  1. Project > Global - project-level skills override global ones
  2. Mode-specific > Generic - skills-code/foo/ overrides skills/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
}

UI Indicators

What Exists

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.

What Doesn't Exist

No dedicated "skill activated" indicator. There's no badge, toast, or status bar showing "Skill X activated".

How to Know a Skill Was Used

  1. read_file tool call - The most visible sign. When the LLM uses a skill, it calls read_file on the SKILL.md path. You'll see this in the chat.
  2. LLM's response text - The LLM might mention "I'm using the translation skill" but this is model-dependent.
  3. 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.

Best Practices for Reliable Triggering

1. Description is Everything

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"

2. Be Specific

Vague descriptions lead to uncertain matching.

3. Use Action Words

Descriptions should describe WHEN to use the skill:

  • "Guidelines for translating and localizing..."
  • "Workflow for deploying to production..."
  • "Process for reviewing pull requests..."

4. Explicit Invocation Always Works

Saying "use the overnight-run skill" will definitely trigger it since the LLM sees the skill name in the list.

5. Test with Variations

The LLM interprets, so "run overnight" might work but "schedule for tonight" might not. Test your skill with different phrasings.

6. Keep Names Lowercase with Hyphens

Per the spec: 1-64 chars, lowercase letters/numbers/hyphens only, no leading/trailing hyphens, no consecutive hyphens.


Code References

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

SKILL.md Structure

---
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment