Inside Claude Code Skills: How a Single Markdown File Powers a Five‑Layer Loading Mechanism

The article dissects Claude Code's Skills system, showing how a lone SKILL.md file, combined with a five‑layer file‑system scope, inode‑based deduplication, conditional activation, plugin integration and incremental injection, enables zero‑code extensibility while managing token consumption for LLM agents.

James' Growth Diary
James' Growth Diary
James' Growth Diary
Inside Claude Code Skills: How a Single Markdown File Powers a Five‑Layer Loading Mechanism

Introduction

James continues his deep‑dive series on Claude Code, moving from the 34‑line Store implementation to the more counter‑intuitive Skills system, which claims that a single Markdown file can extend an AI agent’s capabilities.

Minimal Design

Unlike VSCode, IntelliJ or Chrome extensions that require dozens of files and JSON manifests, Claude Code loads a Skill from one directory containing only SKILL.md. No manifest, no type declarations, no registration center.

Five‑Layer Scope

The loader aggregates skills from five sources, each representing a different authority level:

policySettings – enterprise‑wide policies (environment variable can disable)

userSettings – personal toolbox at ~/.claude/skills/ projectSettings – team‑shared skills in .claude/skills/ additionalDirs – directories added via --add-dir legacyCommands – deprecated command‑based compatibility layer

The loading function loadSkillsFromDir (originally rj6) walks each directory, reads SKILL.md, parses its frontmatter, and builds a Skill object.

Skill Data Structure

name?: string

– displayed name; defaults to directory name description?: string – shown in the skill list model?: string – overrides the default LLM model allowed-tools?: string – comma‑separated whitelist of tools paths?: string[] – gitignore‑style patterns for conditional activation

Three Core Conventions

Directory name equals skill name; no extra config needed.

Only SKILL.md is required; other files may coexist.

Directories lacking SKILL.md are silently skipped.

Inode Deduplication

When the same SKILL.md appears via symbolic links in both ~/.claude/skills/ and .claude/skills/, the loader records the file’s inode in a Map<number, SkillSource>. If an inode has already been processed, the duplicate is ignored, preventing double loading.

const inodeMap = new Map<number, SkillSource>();
for (const {skill, filePath} of allSkills) {
  const inode = await getFileInode(filePath);
  if (inode !== null && inodeMap.has(inode)) {
    log(`Skipping duplicate '${skill.name}' (same file loaded from ${inodeMap.get(inode)})`);
    continue;
  }
  if (inode !== null) inodeMap.set(inode, skill.source);
  // …conditional vs active handling…
}

Conditional Activation

If a skill’s frontmatter defines a paths array, the skill is placed in a “sleeping pool”. When a user touches a file that matches any pattern (using the ignore library with gitignore syntax), the skill is moved to the active list and its content is injected into the system prompt.

function activateConditionalSkills(touchedFiles, projectRoot) {
  const activated = [];
  for (const [skillName, skill] of conditionalSkillsMap) {
    if (!skill.paths?.length) continue;
    const matcher = ignore().add(skill.paths);
    for (const file of touchedFiles) {
      const relPath = path.isAbsolute(file) ? path.relative(projectRoot, file) : file;
      if (matcher.ignores(relPath)) {
        activeSkillsMap.set(skillName, skill);
        conditionalSkillsMap.delete(skillName);
        activated.push(skillName);
        log(`[skills] Activated '${skillName}' (matched: ${relPath})`);
        break;
      }
    }
  }
  if (activated.length) skillsEventEmitter.emit();
  return activated;
}

Typical use cases include a React component‑style guide that activates only on .tsx/.jsx files, a backend API design guide that activates on src/routes/** or src/controllers/**, and a database migration guide that activates on migrations/ directories.

Plugin‑Integrated Skills

When loading external plugins, the entry point loadPluginSkills (originally ph_) supports two modes:

Single‑skill plugin : a pluginDir/SKILL.md file directly represents the plugin’s skill.

Multi‑skill plugin : pluginDir/ contains subdirectories, each with its own SKILL.md, all loaded in batch.

During this process the placeholder ${CLAUDE_SKILL_DIR} in the skill’s markdown is replaced with the absolute path of the skill’s directory, allowing scripts to reference files without hard‑coding locations.

Incremental Skill Injection

The function getSkillListingAttachments (internal name K2z) builds a lightweight attachment that lists only skill names and descriptions. On the first conversation turn the full list is sent; thereafter only newly activated skills are transmitted, avoiding repeated token costs.

const sentSkillSet = sentSkillsRegistry.get(agentId) ?? new Set();
const newSkills = mergedSkills.filter(s => !sentSkillSet.has(s.name));
if (!newSkills.length) return [];
const isInitialLoad = sentSkillSet.size === 0;
newSkills.forEach(skill => sentSkillSet.add(skill.name));
log(`Sending ${newSkills.length} skills via attachment (${isInitialLoad ? "initial" : "dynamic"})`);

SkillTool Execution Chain

When the model decides a skill is needed, it calls SkillTool({skill: "commit"}). The tool then:

Checks that the skill_listing tool is enabled.

Merges user and system skills, filters out already‑sent ones, and sends only the incremental set.

Calls getPromptForCommand, which reads the full SKILL.md, substitutes $ARGUMENTS and ${CLAUDE_SKILL_DIR}, and optionally executes inline shell commands.

Executes either the default inline mode (injecting the markdown directly into the current conversation) or the fork mode (spawning a sub‑agent with its own token budget) based on the frontmatter context field.

Registers the skill name in a session‑wide set so the same skill is not re‑sent.

Writing a Real SKILL.md

A concrete example creates an API‑design guide for a TypeScript full‑stack project. The steps are:

Create the directory mkdir -p .claude/skills/api-design-guide and add an empty SKILL.md.

Populate SKILL.md with frontmatter (name, description, allowed‑tools, paths) and a body that defines RESTful conventions, versioning, error handling, etc.

Verify loading with the /skills command and trigger activation by opening a matching file such as src/routes/user.route.ts, which should log [skills] Activated 'api-design-guide'.

Design Insights

File system as configuration : directory structure replaces explicit manifests; the pattern works for micro‑service discovery, CI task loading, and test fixture registration.

Multi‑scope single loader : five layers share the same loadSkillsFromDir implementation, differing only by the source argument, reducing maintenance overhead.

Conditional activation for token budgeting : separating always‑active and on‑demand skills saves context length, a crucial cost factor for LLM applications.

Inode‑based deduplication : reliable across symbolic links and mount points, unlike path‑based deduplication.

Limitations

The full content of a SKILL.md is injected verbatim, so long skill files can consume thousands of tokens each time they fire. There is currently no summarisation or compression mechanism.

Reusing the Pattern in Your Own Agent

A minimal TypeScript loader (≈30 lines) reads directories, parses frontmatter with gray‑matter, and returns an array of skill objects ready to be formatted into system prompts. The same loader can be adapted for any LLM‑agent framework.

import {readdir, readFile} from "fs/promises";
import {join} from "path";
import matter from "gray-matter";

interface Skill {name: string; description: string; prompt: string; paths?: string[]}

async function loadSkillsFromDir(dir: string): Promise<Skill[]> {
  let entries;
  try { entries = await readdir(dir, {withFileTypes: true}); }
  catch { return []; }
  const skills: Skill[] = [];
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    try {
      const raw = await readFile(join(dir, entry.name, "SKILL.md"), "utf-8");
      const {data: fm, content} = matter(raw);
      skills.push({
        name: fm.name ?? entry.name,
        description: fm.description ?? "",
        prompt: content.trim(),
        paths: fm.paths,
      });
    } catch { continue; }
  }
  return skills;
}

const skills = await loadSkillsFromDir("./.agent/skills");
const systemPrompt = skills.map(s => `## ${s.name}
${s.prompt}`).join("

---

");

Conclusion

The Skills system demonstrates a five‑layer, file‑system‑driven, zero‑code extensibility model where a single Markdown file can define a skill, be conditionally activated, de‑duplicated by inode, and injected incrementally to keep token usage low.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptPlugin IntegrationMarkdownSkillsClaude CodeConditional Activation
James' Growth Diary
Written by

James' Growth Diary

I am James, focusing on AI Agent learning and growth. I continuously update two series: “AI Agent Mastery Path,” which systematically outlines core theories and practices of agents, and “Claude Code Design Philosophy,” which deeply analyzes the design thinking behind top AI tools. Helping you build a solid foundation in the AI era.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.