Course / Lesson 12  ·  PT-BR
Lesson 12 · Deep dive · subsystem 6 of 7

SkillStore — progressive disclosure

Skills are "narrow, actionable procedural memory" — the durable half of the self-improving loop, complementing the broad declarative MemoryStore. A skill is a directory: a SKILL.md (frontmatter + body) plus optional files under references/templates/scripts/assets. The defining idea is progressive disclosure: list() reads only cheap metadata; view() loads the full body on demand. A CLONE of Hermes' skills_tool.py (1638 LOC) + skill_manager_tool.py (1233 LOC).

Three tiers of cost

tier 1 · list() reads ONLY frontmatter name ≤64 · description ≤1024 never the body tier 2 · view(name) full SKILL.md body + list of linked files tier 3 · linked file references/api.md etc. loaded by relative path cheap to list everything; pay for a body only when you open it; pay for a reference only when you need it
// packages/hermes/src/skills/skill-store.ts:80-96 — list (tier 1)
async list(): Promise<Result<readonly SkillMetadata[], Error>> {
  // … readDir(baseDir) …
  for (const entry of entries.value) {
    if (!entry.isDirectory) continue;
    const meta = await this.readMetadata(entry.name);   // frontmatter only
    if (meta.ok) found.push(meta.value);              // skip non-skill dirs silently
  }
  found.sort((a, b) => a.name.localeCompare(b.name));
  return ok(found);
}

A directory without a readable, valid-frontmatter SKILL.md is simply skipped — it isn't a skill (the source's _find_all_skills tolerance). The two metadata caps are carried verbatim: MAX_NAME_LENGTH = 64, MAX_DESCRIPTION_LENGTH = 1024.

The dependency-free frontmatter parser

The source parses frontmatter with full YAML (CSafeLoader), with a naive key: value fallback when YAML errors. To stay dependency-free — adding a yaml dep is a stop-condition for this run — the clone reproduces exactly that fallback and nothing more: top-level scalar pairs only:

// packages/hermes/src/skills/frontmatter.ts:60-70 — parseScalarBlock
for (const line of block.split('\n')) {
  const colon = line.indexOf(':');
  if (colon === -1) continue;               // no colon ⇒ skip the line
  const key = line.slice(0, colon).trim();
  if (key.length === 0) continue;
  out[key] = line.slice(colon + 1).trim();      // split on FIRST colon; later keys win
}

The block-boundary detection mirrors the source: the document must start with ---, and the block closes at the first line that is --- (matched as /\n---[ \t]*\n/ from offset 3). A document without an opening fence returns ({}, content) — the whole thing is the body. Nested mappings, lists, quotes-with-colons are deliberately out of scope.

Path safety: no traversal, under an allowed subdir

Supporting files are confined. A relative path must contain no .. or . segment, must not be absolute, must use forward slashes, and its first segment must be one of the four support subdirs:

// packages/hermes/src/skills/skill-store.ts:404-437 — validateSupportPath (condensed)
if (relPath.includes('\\'))   return err(…'use forward slashes');
if (relPath.startsWith('/')) return err(…'must be relative');
const segments = relPath.split('/').filter((s) => s.length > 0);
if (segments.length < 2)     return err(…"must be under references/templates/scripts/assets");
for (const segment of segments)
  if (segment === '..' || segment === '.') return err(…'path traversal is not allowed');
if (!isSupportDir(segments[0]))    return err(…'first segment must be one of …');
Why so strict? A skill can hold scripts and assets the agent authored; a relative path that escaped the skill directory (../../etc/...) would be a write-anywhere primitive. Confining every supporting-file path under an allowlisted subdir of the one skill turns "write a file" into a bounded, auditable operation. Four tests guard it: a .. escape, an absolute path, a backslash path, and a file outside the allowed subdirs are each refused.

One matcher, reused: unique-substring patch

patch does a targeted find-and-replace by UNIQUE substring — and it deliberately reuses the same fail-closed engine the memory store uses (exact match; zero or multiple occurrences → err), not the source's fuzzy whitespace-normalizing matcher. The choice is fidelity to the shipped Alembic convention over the Python helper:

// packages/hermes/src/skills/skill-store.ts:449-461 — replaceUnique
const first = content.indexOf(find);
if (first === -1) return err(new Error(`No match for '${find}' in SKILL.md.`));
const second = content.indexOf(find, first + find.length);
if (second !== -1) return err(new Error(`Multiple matches for '${find}' …`));
return ok(content.slice(0, first) + replace + content.slice(first + find.length));
A patch can't corrupt the metadata — re-parse after replace

After replacing, patch re-parses the result and re-validates the frontmatter; if the edit broke the SKILL.md structure into invalid metadata, the write is refused with "Patch would break SKILL.md structure" — the edit never lands. The test "refuses a patch that would corrupt the frontmatter" proves it. Two more deviations to note: the FsPort exposes no unlink, so delete/removeFile clear the file to empty (which makes it invisible to list()/view() — empty frontmatter), and create refuses a name collision while view/edit/patch/delete refuse a missing skill.

1. An agent calls list() over a directory of 50 skills. What does it read from disk?
Correct: b. list() reads only frontmatter via readMetadata — never the body. You pay for a body only when you view(name) it, and for a linked file only when you open it. The test "returns metadata only — name + description, never the body" confirms it.
2. writeFile('my-skill', '../../secrets.txt', …) is called. The result?
Correct: c. Every supporting-file path is confined under an allowlisted subdir of the skill, with explicit .././absolute/backslash rejection. This turns "write a file" into a bounded operation, never a write-anywhere primitive.
3. patch is asked to replace a substring that appears twice in SKILL.md. What happens?
Correct: d. replaceUnique requires exactly one occurrence; zero or multiple → err. The skill store deliberately reuses Alembic's unique-substring engine, not the Python fuzzy matcher, for fidelity to the shipped convention.

Common confusions

"It parses full YAML frontmatter." No — only top-level scalar key: value pairs, a faithful clone of the source's fallback path. Richer YAML (nested metadata, lists) is deferred until a human approves a yaml dependency — adding one is a stop-condition for this run.
"delete removes the directory." No — the FsPort has no recursive unlink, so delete clears SKILL.md to empty, which makes the skill invisible to list()/view() (empty frontmatter fails validation). Supporting files are managed individually via removeFile.