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).
// 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 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.
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 …');
../../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.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));
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.
list() over a directory of 50 skills. What does it read from disk?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.writeFile('my-skill', '../../secrets.txt', …) is called. The result?.././absolute/backslash rejection. This turns "write a file" into a bounded operation, never a write-anywhere primitive.patch is asked to replace a substring that appears twice in SKILL.md. What happens?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.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.