Skip to content

Skills: instrucciones Markdown

源码版本v2026.6.11

Responsabilidad

Una skill en OpenClaw no es una función invocable, sino una instrucción Markdown — concretamente, un archivo SKILL.md. Al construir el system prompt, el sistema lista el name/description/location de todas las skills disponibles como un bloque XML <available_skills> dentro del prompt; cuando el modelo ve que una tarea encaja con la descripción de una skill, usa la herramienta read para leer el SKILL.md correspondiente y sigue los pasos descritos.

El mecanismo parece «ligero», pero se diferencia esencialmente del sistema de herramientas: las herramientas son funciones registradas por el sistema, y el modelo solo puede invocar el nombre; las skills son documentos que el propio modelo decide si leer. El contenido de una skill puede ser cualquier Markdown — pasos de operación, checklists, glosarios, plantillas de código —; tras leerlo, el modelo usa herramientas si hace falta o escribe código si hace falta; la skill no participa en la invocación runtime.

El sistema de skills se ocupa por tanto de cuatro cosas: descubrimiento (discovery) encuentra todos los archivos SKILL.md, carga (loading) parsea el frontmatter en un objeto Skill, inyección (injection) inserta el resumen en el system prompt, y ciclo de vida (lifecycle) gestiona la instalación/archivado/subida de skills. El resto — «leer o no, cómo usar» — lo decide el modelo.

Motivación de diseño

¿Por qué no convertir también las skills en herramientas? Porque la invocación de herramientas es determinista — el schema de parámetros, la validación de entrada y la lógica del executor son fijos, y cada invocación del mismo nombre de herramienta se comporta de forma consistente. Pero muchas guías de tarea son naturalmente narrativas: «ante un bug, primero mira el log, luego reproduce, luego localiza» — este flujo no se puede meter en un JSON schema. Que el modelo comprenda la guía en lenguaje natural y luego combine invocaciones de herramientas con flexibilidad queda mejor que forzarlo a una función.

Otra motivación es la carga perezosa (lazy loading). El cuerpo de una skill puede ser largo (decenas de KB de manual de operación), pero el system prompt solo necesita el resumen. El modelo solo read el contenido completo cuando la tarea encaja; en la mayoría de las conversaciones, el cuerpo de la skill no entra en la ventana de contexto. formatSkillsForPrompt en skill-contract.ts formatSkillsForPrompt:L34-L58 solo emite cuatro campos — name/description/location/version — por esta razón.

El campo <version> es un diseño clave: es un marcador estable del contenido de SKILL.md. El system prompt dice explícitamente al modelo: «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) — el autor de la skill cambió el contenido; en la siguiente ronda, el modelo debería releer en lugar de fiarse de la memoria de la versión vieja.

El campo source conserva la marca de origen (openclaw-bundled / workspace / plugin, etc.), y el campo frontmatter disableModelInvocation permite al autor de la skill declarar «el modelo no puede invocarla automáticamente; solo el usuario puede dispararla explícitamente» — una salida para operaciones de alto riesgo que requieren permiso explícito.

Archivos clave

Flujo de datos

La carga de skills empieza en el sistema de archivos (local-loader.ts carga directorio único:L38-L89):

typescript
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,
  };
}

Tres compuertas: openRootFileSync (src/skills/loading/local-loader.ts:L16-L36) resuelve la ruta contra la ruta real de la raíz, evitando que un symlink escape del directorio de la skill; un fallo al parsear el frontmatter descarta silenciosamente la skill, sin que una skill rota arrastre toda la carga; name y description deben existir, sin uno la skill se salta. computeSkillPromptVersion calcula un marcador estable del contenido, que luego va al campo <version>.

loadSkillsFromDirSafe (src/skills/loading/local-loader.ts:L107-L151) es la envoltura superior: primero prueba a cargar el directorio entrante como skill única; si falla, lo trata como directorio padre y enumera subdirectorios para cargar skills en lote. Esta lógica dual «primero mirar si soy una skill, luego si soy una colección» permite que funcionen tanto el directorio raíz skills/ (skill única) como skills/coding-agent/ (sub-skill).

El array de Skill cargado se inyecta al final en system-prompt.ts inyección:L70-L74:

typescript
// Append skills section (only if read tool is available)
const customPromptHasRead = !selectedTools || selectedTools.includes("read");
if (customPromptHasRead && skills.length > 0) {
  prompt += formatSkillsForPrompt(skills);
}

Aquí hay un guardián clave: la sección de skills solo se inyecta si read está en la lista de herramientas activas. En caso contrario, el modelo vería <available_skills> pero no podría leer SKILL.md, lo que crearía la frustración de «saber que existen pero no poder invocarlas». La salida de formatSkillsForPrompt (src/skills/loading/skill-contract.ts:L34-L58) es un XML del tipo:

typescript
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>");

Nótese la tercera línea, que explícitamente dice al modelo: «If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.» — si la versión cambió, releer, no fiarse de la memoria.

Cuando una skill es disparada por el usuario explícitamente (vía chat command) o el modelo decide leer, entra en tool-dispatch.ts resolveSkillDispatchTools:L59-L249. Esta función no «ejecuta la skill», sino que «filtra el conjunto de herramientas según el contexto disparado por la skill» — por ejemplo, cuando se dispara la skill coding-agent, el bucle principal debe configurar qué herramientas se entregan al sub-agent, recorriendo múltiples políticas profile/global/group/sender/sandbox/subagent/inherited:

typescript
const tools = createOpenClawTools({
  agentSessionKey: params.sessionKey,
  // ... mucho contexto
  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({ /* ... */ }),
});

El comentario dice «Keep this aligned with the normal tool surfaces so GHSA-mhm4-93fw-4qr2 stays closed across allow/deny, group, sandbox, and subagent policy layers» — es un seam de seguridad: las invocaciones de herramientas disparadas por skills deben recorrer la misma pipeline de políticas que las invocaciones normales, sin saltarse allowlist por el mero hecho de venir de una skill.

Límites y fallos

  • Ausencia de name/description descarta la skill: en loadSingleSkillDirectory, if (!name || !description) return null; (src/skills/loading/local-loader.ts:L64-L66). Una skill sin description es invisible para el modelo, que no sabría cuándo usarla; en la fase de carga se descarta directamente.
  • Fallo de parseo del frontmatter se descarta: try { parseFrontmatter(raw) } catch { return null; } — un frontmatter roto no arrastra toda la carga, pero esa skill queda invisible. loadSkillsFromDirSafe convierte todos los fallos en «la skill no aparece» en lugar de lanzar error, garantizando disponibilidad global.
  • Defensa contra escape por symlink: readSkillFileSync abre los archivos dentro del límite de la ruta real de la raíz de la skill con openRootFileSync (src/skills/loading/local-loader.ts:L16-L36). Evita que un directorio de skill malicioso coloque un symlink a /etc/passwd y que la herramienta read lo saque.
  • Si la herramienta read no está activa, no se inyecta: hasRead && skills.length > 0 es una condición dura para la inyección (src/agents/sessions/system-prompt.ts:L71-L74). Si la whitelist del usuario cortó read, el sistema no le dice al modelo que hay skills — evita «ver pero no poder comer».
  • disableModelInvocation: resolveSkillInvocationPolicy(frontmatter) parsea este campo. Algunas skills de alto riesgo (por ejemplo, una skill que ejecuta shell arbitrario) pueden declarar «el modelo no puede invocarla automáticamente; requiere un chat command explícito del usuario»; el sistema no la incluye en <available_skills>.
  • Detección de drift de promptVersion: computeSkillPromptVersion calcula un marcador estable del contenido de SKILL.md; si la versión cambia, el modelo debe releer en la siguiente ronda. Es para escenarios de hot reload: el desarrollador cambia el contenido de la skill y en la nueva ronda de diálogo el modelo no se fía de la versión vieja.
  • La skill no es el executor de la herramienta: resolveSkillDispatchTools parece «ejecutar la skill», pero en realidad solo filtra el conjunto de herramientas; quien de verdad «trabaja según la skill» sigue siendo el modelo + las herramientas. El sistema de skills nunca invoca herramientas directamente — esta es la frontera más importante con el sistema de herramientas.

Resumen

Las skills son instrucciones Markdown inyectadas en el system prompt, no funciones invocables. La carga defiende contra escapes por symlink, descarta fallos de parseo de frontmatter de forma tolerante a fallos, y detecta drift de contenido con promptVersion; la inyección solo inserta el resumen <available_skills> cuando la herramienta read está disponible — el cuerpo lo lee el modelo bajo demanda. Las invocaciones de herramientas disparadas por skills recorren la pipeline de múltiples políticas de resolveSkillDispatchTools, compartiendo las mismas reglas allow/deny que las invocaciones normales — esta seam mantiene cerrado GHSA-mhm4-93fw-4qr2. La diferencia con herramientas: las herramientas son funciones en un Map; las skills son instrucciones Markdown. La diferencia con plugins: los plugins son paquetes de extensión descubiertos por convención de directorio; las skills son un tipo de contenido que un plugin puede llevar (campo skills/SKILL.md).

Referencias oficiales: Documentación de skills · README.