Fertigkeiten: Markdown-Anleitungen
Verantwortung
Eine Fertigkeit (skill) ist in OpenClaw keine aufrufbare Funktion, sondern eine Markdown-Anleitung — konkret eine SKILL.md-Datei. Beim Aufbau des System-Prompts listet das System alle verfügbaren Fertigkeiten mit name/description/location in einem <available_skills>-XML und fügt es in den Prompt ein. Sieht das Modell eine Aufgabe, die zur description einer Fertigkeit passt, liest es die SKILL.md selbst mit dem read-Werkzeug und befolgt die dort beschriebenen Schritte.
Dieser Mechanismus wirkt „leicht", unterscheidet sich aber fundamental vom Werkzeugsystem: Werkzeuge sind vom System registrierte Funktionen, die das Modell nur beim Namen aufrufen kann; Fertigkeiten sind Dokumente, bei denen das Modell selbst entscheidet, ob es sie liest. Der Inhalt einer Fertigkeit kann beliebiges Markdown sein — Bedienungsanleitungen, Checklisten, Glossare, Code-Templates — nach dem Lesen nutzt das Modell Werkzeuge oder schreibt Code, wie es passt; die Fertigkeit selbst nimmt nicht an Laufzeitaufrufen teil.
Das Fertigkeits-System hat daher eine klare Aufgabe: Entdecken (discovery) aller SKILL.md-Dateien, Laden (loading) des Frontmatters in ein Skill-Objekt, Injizieren (injection) der Zusammenfassung in den System-Prompt, Lebenszyklus (lifecycle) verwalten Installation/Archivierung/Upload der Fertigkeit. Das Modell übernimmt den Rest „ob lesen, wie verwenden".
Designmotivation
Warum Fertigkeiten nicht auch zu Werkzeugen machen? Weil Werkzeugaufrufe deterministisch sind — Parameter-Schema, Eingabevalidierung, Executor-Logik müssen fixiert sein; jeder Aufruf desselben Werkzeugs verhält sich identisch. Viele Aufgabenanleitungen sind aber natürlichsprachlich narrativ: „Bei einem Bug zuerst ins Log schauen, dann reproduzieren, dann lokalisieren" lässt sich nicht in ein JSON-Schema quetschen. Das Modell die Anleitung in natürlicher Sprache verstehen zu lassen und dann Werkzeugaufrufe flexibel zu kombinieren, passt besser, als es als Funktion zu zwingen.
Ein weiteres Motiv ist Lazy-Laden (lazy loading). Der Fertigkeitstext kann lang sein (mehrere zehn KB Bedienungsanleitung); im System-Prompt reicht es, die Zusammenfassung zu injizieren. Das Modell readet den Volltext nur, wenn die Aufgabe tatsächlich passt; in den meisten Konversationen gelangt der Fertigkeitstext gar nicht ins Kontextfenster. formatSkillsForPrompt gibt in skill-contract.ts formatSkillsForPrompt:L34-L58 nur vier Felder aus: name/description/location/version.
Das Feld <version> ist ein Schlüsseldesign: ein stabiler Marker des SKILL.md-Inhalts. Der System-Prompt teilt dem Modell explizit mit: „If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it." (src/skills/loading/skill-contract.ts:L41-L43) — ändert der Fertigkeitsautor den Inhalt, sollte das Modell in der nächsten Runde neu lesen, anstatt nach alter Erinnerung zu handeln.
Das Feld source speichert die Herkunftskennzeichnung (openclaw-bundled / workspace / plugin u. a.); das Frontmatter-Feld disableModelInvocation erlaubt dem Autor die Erklärung „das Modell darf nicht automatisch aufrufen, nur Nutzer darf explizit auslösen" — das ist für einige hochriskante Operationen eine Eröffnung, die ausdrückliche Erlaubnis erfordern.
Schlüsseldateien
skill-contract.ts Skill:L5-L15—Skill-Typdefinition, enthält name/description/filePath/baseDir/promptVersion/sourceInfo.skill-contract.ts formatSkillsForPrompt:L34-L58— Formatiert das Skill-Array als<available_skills>-XML für den Prompt.local-loader.ts loadSingleSkillDirectory:L38-L89— Laden eines einzelnen Fertigkeitsverzeichnisses: SKILL.md lesen, Frontmatter parsen, Skill zusammenbauen.local-loader.ts loadSkillsFromDirSafe:L107-L151— Batch-Lade-Eintritt; bei Fehler Diagnostics, kein Throw.local-loader.ts readSkillFileSync:L16-L36— Liest Datei überopenRootFileSyncinnerhalb der Fertigkeits-Root-Grenze, verhindert Symlink-Escape.system-prompt.ts Fertigkeitsinjektion:L70-L74— Beim System-Prompt-Aufbau wird die Fertigkeitsliste überformatSkillsForPrompteingebaut.system-prompt.ts hasRead-Wächter:L169-L172— Nur wenn das read-Werkzeug verfügbar ist, wird der Fertigkeitsabschnitt injiziert; sonst kann das Modell ohnehin nicht lesen.tool-dispatch.ts resolveSkillDispatchTools:L59-L249— Beim durch Fertigkeit ausgelösten Werkzeugaufruf wird die Werkzeugmenge nach mehrschichtiger Strategie gefiltert.install.ts installSkill:L458— Fertigkeitsinstallations-Eintritt, zieht Fertigkeit aus Archiv/Source/Upload.openclaw-provider-index.ts:L13-L69— Eingebauter Provider-Index, analog zur Fertigkeits-Vordeklaration.
Datenfluss
Das Laden von Fertigkeiten beginnt im Dateisystem (local-loader.ts einzelnes Verzeichnis laden:L38-L89):
function loadSingleSkillDirectory(params: {
skillDir: string;
source: string;
rootRealPath: string;
maxBytes?: number;
}): LoadedLocalSkill | null {
const skillFilePath = path.join(params.skillDir, "SKILL.md");
const raw = readSkillFileSync({
rootRealPath: params.rootRealPath,
filePath: skillFilePath,
maxBytes: params.maxBytes,
});
if (!raw) {
return null;
}
let frontmatter: Record<string, string>;
try {
frontmatter = parseFrontmatter(raw);
} catch {
return null;
}
const fallbackName = path.basename(params.skillDir).trim();
const name = frontmatter.name?.trim() || fallbackName;
const description = frontmatter.description?.trim();
if (!name || !description) {
return null;
}
const invocation = resolveSkillInvocationPolicy(frontmatter);
// ...
return {
skill: {
name,
description,
filePath,
baseDir,
promptVersion: computeSkillPromptVersion(raw),
source: params.source,
sourceInfo: createSyntheticSourceInfo(filePath, { /* ... */ }),
disableModelInvocation: invocation.disableModelInvocation,
},
frontmatter,
};
}Drei Schleusen: Datei über openRootFileSync lesen (src/skills/loading/local-loader.ts:L16-L36) löst den Pfad auf den realen Root-Pfad auf, verhindert, dass ein Symlink aus dem Fertigkeitsverzeichnis ausbricht; Frontmatter-Parse-Fehler verwirft stillschweigend diese Fertigkeit, eine kaputte SKILL.md blockiert nicht den gesamten Ladevorgang; name und description müssen beide vorhanden sein, fehlt eines, wird sie übersprungen. computeSkillPromptVersion berechnet einen stabilen Marker über den Inhalt, der später ins Feld <version> eingeht.
loadSkillsFromDirSafe (src/skills/loading/local-loader.ts:L107-L151) ist der höhere Wrapper: Versucht zuerst, das übergebene Verzeichnis als einzelnes Fertigkeitsverzeichnis zu laden; scheitert das, behandelt es als Elternverzeichnis und enumeriert Unterverzeichnisse im Batch. Diese双层 Logik „erst prüfen, ob es selbst eine Fertigkeit ist, dann ob es eine Fertigkeitssammlung ist" lässt sowohl das Layout skills/ (Wurzel ist einzelne Fertigkeit) als auch skills/coding-agent/ (Unterverzeichnis ist Fertigkeit) funktionieren.
Das fertige Skill-Array wird schließlich in system-prompt.ts Injektion:L70-L74 in den Prompt eingebaut:
// Append skills section (only if read tool is available)
const customPromptHasRead = !selectedTools || selectedTools.includes("read");
if (customPromptHasRead && skills.length > 0) {
prompt += formatSkillsForPrompt(skills);
}Hier ist ein kritischer Wächter: Nur wenn das read-Werkzeug in der aktiven Liste ist, wird der Fertigkeitsabschnitt injiziert. Sonst sieht das Modell <available_skills>, kann aber SKILL.md nicht lesen und geriete in die Verlegenheit „sichtbar, aber nicht greifbar". formatSkillsForPrompt (src/skills/loading/skill-contract.ts:L34-L58) produziert ungefähr folgendes XML:
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
"If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.",
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use the absolute path in tool commands.",
"",
"<available_skills>",
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${escapeXml(skill.name)}</name>`);
lines.push(` <description>${escapeXml(skill.description)}</description>`);
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
if (skill.promptVersion) {
lines.push(` <version>${escapeXml(skill.promptVersion)}</version>`);
}
lines.push(" </skill>");
}
lines.push("</available_skills>");Die dritte Zeile teilt dem Modell explizit mit: „If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it." — Version geändert, neu lesen, nicht nach Erinnerung handeln.
Wird eine Fertigkeit vom Nutzer explizit ausgelöst (über Chat-Kommando) oder entscheidet das Modell automatisch zu lesen, wird tool-dispatch.ts resolveSkillDispatchTools:L59-L249 betreten. Diese Funktion „führt nicht die Fertigkeit aus", sondern „filtert die Werkzeugmenge nach dem Kontext der Fertigkeitsauslösung" — wenn z. B. die coding-agent-Fertigkeit ausgelöst wird, muss die Hauptschleife dem Sub-Agent eine Werkzeugmenge geben, die durch die mehrschichtige Strategie profile/global/group/sender/sandbox/subagent/inherited gefiltert wird:
const tools = createOpenClawTools({
agentSessionKey: params.sessionKey,
// ... viele Kontexte
pluginToolAllowlist: collectExplicitAllowlist(explicitPolicyList),
pluginToolDenylist: explicitDenylist,
// ...
});
const policyFiltered = applyToolPolicyPipeline({
tools,
toolMeta: (tool) => getPluginToolMeta(tool),
warn: logVerbose,
steps: [
...buildDefaultToolPolicyPipelineSteps({ /* profile/provider/global/agent/group/sender */ }),
{ policy: sandboxPolicy, label: "sandbox tools.allow" },
{ policy: subagentPolicy, label: "subagent tools.allow" },
{ policy: inheritedToolPolicy, label: "inherited tools" },
],
declaredToolAllowlist: buildDeclaredToolAllowlistContext({ /* ... */ }),
});Im Kommentar steht „Keep this aligned with the normal tool surfaces so GHSA-mhm4-93fw-4qr2 stays closed across allow/deny, group, sandbox, and subagent policy layers" — das ist eine Sicherheitsnaht; durch Fertigkeit ausgelöste Werkzeugaufrufe müssen dieselbe Strategie-Pipeline durchlaufen wie normale Werkzeugaufrufe und dürfen Allowlist nicht umgehen, nur weil sie „durch Fertigkeit ausgelöst" wurden.
Grenzen und Fehler
- name/description fehlen → verwerfen:
loadSingleSkillDirectorymitif (!name || !description) return null;(src/skills/loading/local-loader.ts:L64-L66). Hat eine Fertigkeit keine description, weiß das Modell nicht, wann es sie nutzen soll — sie ist nutzlos und wird beim Laden verworfen. - Frontmatter-Parse-Fehler verworfen:
try { parseFrontmatter(raw) } catch { return null; }— ein kaputter Frontmatter blockiert nicht den gesamten Ladevorgang, aber diese Fertigkeit ist unsichtbar.loadSkillsFromDirSafewandelt alle Fehler in „Fertigkeit erscheint nicht" um, ohne zu werfen; die Gesamtverfügbarkeit bleibt erhalten. - Symlink-Escape-Verteidigung:
readSkillFileSyncöffnet überopenRootFileSyncDateien innerhalb der realen Root-Grenze der Fertigkeit (src/skills/loading/local-loader.ts:L16-L36). Das verhindert, dass ein bösartiges Fertigkeitsverzeichnis einen Symlink auf/etc/passwdlegt und dieser dann über dasread-Werkzeug ausgelesen wird. - read-Werkzeug nicht aktiv → nicht injizieren:
hasRead && skills.length > 0ist die harte Bedingung (src/agents/sessions/system-prompt.ts:L71-L74). Hat der Nutzer dasread-Werkzeug aus der Whitelist gestrichen, teilt das System dem Modell nicht mit, dass es Fertigkeiten gibt — vermeidet „sichtbar, aber nicht greifbar". - disableModelInvocation:
resolveSkillInvocationPolicy(frontmatter)parst dieses Feld. Bestimmte hochriskante Fertigkeiten (z. B. Fertigkeit, die beliebigen Shell ausführt) kann der Autor als „Modell darf nicht automatisch aufrufen, nur Nutzer über Chat-Kommando" deklarieren; das System nimmt diese Fertigkeit nicht in<available_skills>auf. - promptVersion-Drift-Erkennung:
computeSkillPromptVersionberechnet einen stabilen Marker über den SKILL.md-Inhalt; hat sich die Version geändert, sollte das Modell in der nächsten Runde neu lesen. Das ist für Hot-Reload-Szenarien gedacht: Ändert ein Entwickler den Fertigkeitsinhalt, handelt das Modell in der neuen Runde nicht nach alter Versionserinnerung. - Fertigkeit ist nicht die Ausführung eines Werkzeugs:
resolveSkillDispatchToolssieht aus wie „Fertigkeit ausführen", macht aber nur Werkzeugmengen-Filterung; „nach Fertigkeit arbeiten" tun weiterhin Modell + Werkzeuge. Das Fertigkeitssystem ruft nie direkt Werkzeuge auf — das ist die größte Grenze zum Werkzeugsystem.
Zusammenfassung
Fertigkeiten sind Markdown-Anleitungen, die in den System-Prompt injiziert werden, keine aufrufbaren Funktionen. Die Ladephase nutzt Grenzen-Lesen, um Symlink-Escape zu verhindern, Frontmatter-Parse-Fehler verwirft fehlertolerant und promptVersion erkennt Inhaltsdrift; die Injektionsphase injiziert die <available_skills>-Zusammenfassung nur, wenn das read-Werkzeug verfügbar ist; der Volltext wird vom Modell bei Bedarf read. Durch Fertigkeit ausgelöste Werkzeugaufrufe durchlaufen resolveSkillDispatchTools' mehrschichtige Strategie-Pipeline und teilen sich Allow/Deny-Regeln mit normalen Werkzeugaufrufen — diese Naht hält GHSA-mhm4-93fw-4qr2 geschlossen. Unterschied zu Werkzeugen: Werkzeuge sind Funktionen in der Map; Fertigkeiten sind Markdown-Anleitungen. Unterschied zu Plugins: Plugins sind nach Verzeichniskonvention entdeckte Erweiterungspakete; Fertigkeiten sind eine Inhaltsform, die ein Plugin tragen kann (Plugin-Feld skills/SKILL.md).
Vergleich mit offiziellen Ressourcen: Skills-Doku · README.