Skill Tool — Cross-Language Design Spec
Status: Draft v1 — 2026-04-22
Implementations: TypeScript (@agent-sh/harness-skill, pending), Rust (crates/skill, pending)
Scope: Language-neutral contract. Implementation files (packages/skill/ for TS, crates/skill/ for Rust) must conform.
This spec is the source of truth. Implementation-specific ergonomics are allowed; public semantics are not.
Prior art surveyed: Agent Skills open spec (agentskills.io), Claude Code Skill tool, OpenCode skill.ts, Gemini CLI activate-skill, Continue readSkill, Codex CLI core-skills, Anthropic API skill execution, and 35+ adopter harnesses. Research summary in agent-knowledge/skill-tool-design-across-harnesses.md and agent-knowledge/skill-tool-in-autonomous-agents.md.
1. Purpose
Expose authored skills to an LLM as a structured tool. A skill is a folder — skill-name/SKILL.md with YAML frontmatter plus optional scripts/, references/, assets/ — that encodes specialized workflows, project conventions, or compressed expertise. The tool's job is to:
- Enumerate installed skills as a low-cost catalog (≤100 tokens per skill, always available).
- Expand a skill's full body into the conversation on demand, exactly once per activation.
- Enforce that skills are authored files, not runtime-generated prompts — you can commit them, diff them, review them.
- Interoperate with the rest of the tool surface:
allowed-tools, scripts invoked viabash, references opened viaread.
Scope note: v1 ships the authored-skill pattern only. Runtime-learned skills (Voyager / Letta-style) are out of scope — they belong in a separate @agent-sh/harness-skill-learner adapter package if demand appears.
Enforce at the tool layer every invariant the model cannot be trusted on:
- Name matches directory.
name: fooinsideskills/bar/SKILL.mdis rejected. - Frontmatter validates. Malformed YAML → structured error, not silently dropped.
- Discovery paths are bounded. Only session-configured roots are scanned; no walking up to
/. - Activation is permission-gated. Every activation passes through the permission hook, carrying the skill's
allowed-toolsdeclaration as metadata. - Trust gating for autonomous mode. Project-root skills from untrusted repos require hook approval or explicit opt-in;
unsafeAllowSkillWithoutHookfor fixtures. - Dedupe. Re-activating the same skill in the same session is a no-op (returns a "already loaded" marker), not a context-bloat replay.
- Compaction-safe body wrapping. Skill bodies return wrapped in a stable structural marker so auto-compaction preserves them correctly.
Non-goals for v1:
- Subagent-forked skills (Claude Code's
context: fork+agent: <type>). Requires our own subagent primitive, which we don't ship. - Dynamic shell injection in skill bodies (Claude Code's
!`<cmd>`backtick blocks). Security surface; needs careful design. - File-path gating for auto-activation (
paths: ["**/*.py"]). Requires file-context awareness we don't have. - Model/effort overrides (
model: opus,effort: high). These are harness concerns, not tool concerns. - Per-skill hooks (skill-scoped
PreToolUse/PostToolUse). Needs design round. - Live filesystem watching for in-session skill changes. Nice-to-have; not load-bearing.
2. Input contract
{
name: string // required, matches an installed skill
arguments?: string | Record<string,str> // optional, positional or named
}
Deliberate omissions:
- No
path. The tool dispatches onname, not on a filesystem path. The harness owns discovery; the model chooses by name. - No
scope. Skills are session-scoped once loaded; there's no per-turn / per-tool-call scope. - No
reload: true. Session-lifetime contract is simpler; if the skill author edits mid-session, they invalidate by re-running. - No free-form
inputmap.argumentsis the Claude Code$ARGUMENTS/$N/$nameconventions; if a skill declares namedargumentsin its frontmatter, the tool validates against that declaration.
Parameter validation
namenot a string, empty, or > 64 chars →INVALID_PARAM.namedoes not match/^[a-z0-9]+(-[a-z0-9]+)*$/→INVALID_PARAM: "skill name must be lowercase-kebab-case".namenot in the session's installed-skill catalog →NOT_FOUND, withsiblingslisting fuzzy-matched installed names.argumentsis a string AND the skill declares namedargumentsin its frontmatter →INVALID_PARAM: "skill{name}expects named arguments: {list}".argumentsis a map AND the skill declares noargumentsfrontmatter →INVALID_PARAM: "skill{name}does not accept named arguments".
2.1 Known-alias pushback
Mirrors the pattern from bash / webfetch / grep / glob. Required alias set:
skill,skill_name,name_of_skill,slug→nameinvoke,activate,run→ drop; the tool activates implicitlyargs,args_string,input,params,parameters→argumentscontext,session→ drop with note "skill context is session-scoped; no per-call override"reload,fresh,force_reload,refresh→ drop with note "skills load once per session; edit the skill file and restart the session to refresh"fork,subagent,isolated→ drop with v1.1 note "subagent-forked skills deferred to v1.1"paths,scope_paths→ drop with v1.1 notemodel,effort→ drop with v1.1 note "model / effort overrides are harness concerns, not tool parameters"file,file_path,skill_path,dir→ drop with note "skills dispatch by name, not path; the harness owns discovery"
2.2 Description guidance (model-facing)
Tool description must call out:
Activate an installed skill by name. A skill is a reusable package of instructions, optional scripts, and reference docs, authored as a folder at
skill-name/SKILL.md. Activating loads the skill's body into the conversation for the rest of the session.When to use. Activate a skill when the user's request matches its description. The catalog of installed skills, each with name and short description, is always visible in your tool-call context. If two skills plausibly apply, pick the one whose description most precisely matches.
Idempotence. Activating the same skill twice in one session is a no-op — the body is already loaded. The tool returns a
already_loadedmarker so you know the content is still in context.Arguments. Pass
argumentsas a string for positional skills (those declaring$ARGUMENTSor$1/$2) or as a JSON object for skills that declare named arguments in frontmatter. Run without arguments if the skill doesn't need them.Permission. Activation runs through the session's permission hook. A skill's
allowed-toolsfrontmatter is an advisory declaration of what tools it expects to need — it does not pre-approve anything; downstream tool calls still pass the session's permission hook.
Research backing: Anthropic's Oct 2025 Agent Skills announcement emphasizes progressive disclosure ("load on demand, not always"); Simon Willison documents that skill metadata costs 50-100 tokens vs MCP's 10,000+ for identical capability exposure.
3. Output contract
Output is a discriminated union by kind.
3.1 kind: "ok" — skill activated, body loaded
<skill name="{name}" dir="{skill_dir}">
<frontmatter>
{the parsed frontmatter, re-serialized minus body}
</frontmatter>
<instructions>
{the SKILL.md body — everything after the closing ---}
</instructions>
<resources>
{optional; up to 10 sampled file names from scripts/ references/ assets/ — names only, not contents}
</resources>
</skill>
(Skill "{name}" activated. Body is {N} bytes. Scripts available via bash(<skill-dir>/scripts/<name>). References via read(<skill-dir>/references/<name>).)
The <skill> XML wrapper is load-bearing for auto-compaction: harnesses that summarize history can recognize the marker and preserve the full body verbatim (matching Claude Code's 5000-per-skill / 25000-total compaction carryforward policy).
Structured result shape (what TS/Rust return):
{
kind: "ok",
name: string,
dir: string, // absolute path to the skill directory
body: string, // the markdown body, frontmatter-stripped
frontmatter: Record<string,unknown>, // parsed YAML
resources: string[], // up to 10 filenames from scripts/ references/ assets/
bytes: number,
output: string // the rendered <skill>…</skill> block above
}
3.2 kind: "already_loaded" — skill is in-context from a prior call
(Skill "{name}" is already active in this session. Body was loaded at turn {turn}. No new content was added.)
Structured: { kind: "already_loaded", name, at_turn }.
This is idempotence. The model may call skill with the same name twice without burning context; the second call is a short no-op hint.
3.3 kind: "not_found"
(No skill matches "{name}". Did you mean: {siblings}? Run with a listed name from the catalog.)
Structured: { kind: "not_found", name, siblings: string[] }.
Siblings are Levenshtein-ranked installed skill names (top 3, similarity threshold 0.6). Mirrors read's NOT_FOUND fuzzy behavior.
3.4 kind: "error"
Structured errors, not thrown. Format: Error [CODE]: message.
code |
When |
|---|---|
INVALID_PARAM |
Schema error, alias pushback, bad argument shape. |
NOT_FOUND |
Skill name does not exist in any configured skill root. |
SENSITIVE |
Skill dir matches sensitive-patterns (e.g. **/.env/**) and no hook approved. |
OUTSIDE_WORKSPACE |
Skill dir resolved outside all configured workspace roots with no hook approval. |
INVALID_FRONTMATTER |
YAML parse error, missing required fields, or constraints violated. |
NAME_MISMATCH |
name: foo inside skills/bar/SKILL.md. Rejected with the path + declared name. |
DISABLED |
Skill has disable-model-invocation: true and the activation came from the model, not the user. |
NOT_TRUSTED |
Project-root skill from an untrusted dir; session required hook approval; hook denied or returned ask. |
PERMISSION_DENIED |
Hook explicitly denied activation. |
IO_ERROR |
Filesystem read of SKILL.md or resource enumeration failed. |
Error messages echo the requested name:
Error [INVALID_FRONTMATTER]: skill "my-skill" has malformed YAML frontmatter.
Path: /workspace/.skills/my-skill/SKILL.md
Reason: expected colon after 'description' on line 4
Hint: frontmatter must be valid YAML between two `---` lines at the top of the file. See agentskills.io/specification.
4. Frontmatter schema
The frontmatter contract is this spec's primary surface. Parity with the agentskills.io open standard plus a minimal set of extensions widely adopted in Claude Code and in the project author's own 66-file skill corpus.
4.1 Required fields
| Field | Constraint | Notes |
|---|---|---|
name |
1-64 chars, ^[a-z0-9]+(-[a-z0-9]+)*$, must equal the containing directory's basename |
Matches agentskills.io spec verbatim. |
description |
1-1024 chars | Must include what the skill does AND when to use it. The description drives both catalog discovery and model activation choice. |
4.2 Optional fields — standardized
| Field | Shape | Notes |
|---|---|---|
version |
semver string (1.0.0, 2.1.3-beta) |
Not in agentskills.io spec but present in 71% of our corpus; de facto universal. Recommended for any skill shipped publicly. |
argument-hint |
string, ≤ 200 chars | Autocomplete hint shown in slash-menus ([path] [--fix]). Present in 42% of our corpus. |
license |
SPDX identifier (MIT, Apache-2.0) or bundled filename (LICENSE) |
Per spec. |
compatibility |
string, 1-500 chars | Free-form environment requirements (node >= 20, requires ripgrep). Per spec. |
metadata |
object, string → string | Arbitrary client-extension key/value map. Per spec. Our parser passes through verbatim. |
4.3 Optional fields — behavior hints
| Field | Shape | Semantics | v1? |
|---|---|---|---|
allowed-tools |
comma- or space-separated string (Read, Grep, Bash(git:*)) |
Advisory only in v1 — declares what tools the skill expects to need. See §6 for composition semantics. | yes |
disable-model-invocation |
boolean (default false) |
When true, only user-initiated /name invocations load the skill. Model calls to skill({ name }) return kind: "disabled". |
yes |
user-invocable |
boolean (default true) |
When false, the skill is hidden from any slash-menu catalog the harness may render. Model-only skills. |
yes |
arguments |
object, declaring named positional args for $name substitution |
Opt-in named-argument contract. If declared, arguments input to the tool must be an object whose keys match this declaration. |
optional v1 |
4.4 Optional fields — deferred to v1.1
These are Claude Code extensions we explicitly defer. Parser must ignore unknown fields, not reject, to keep forward-compatibility with skill authors who author for multiple harnesses.
| Field | Why deferred |
|---|---|
context: fork |
Requires a subagent primitive we don't ship. |
agent: <type> |
Same. |
hooks |
Skill-scoped hooks need a hook runtime design pass. |
model |
Model choice is a harness concern, not a tool concern. |
effort |
Same. |
paths |
Auto-activation gating on file context; we don't track file context. |
shell |
Only meaningful once we add dynamic !`<cmd>` injection. |
when_to_use |
The description field carries this already. |
4.5 Parser rules
- Frontmatter is YAML between two
---lines at the top of the file. Missing opening---→ no frontmatter (body is the whole file). Missing closing---→INVALID_FRONTMATTER. - Parse YAML into a
Record<string, unknown>. Validate required fields first. - Validate
namematches the containing directory's basename.name: fooinsideskills/bar/SKILL.md→NAME_MISMATCH. - Normalize
allowed-toolsfrom either string or array form intostring[]. - Unknown fields are preserved in the output's
frontmatterfield; the parser does not reject them. - The body (everything after the closing
---) is stored verbatim — no markdown processing. The model sees it raw.
4.6 Canonical SKILL.md
---
# Required
name: api-conventions
description: |
Use when designing or auditing HTTP APIs in this project. Covers endpoint naming,
error envelope shape, pagination, and deprecation conventions. Run before drafting
new routes or when reviewing PRs that add endpoints.
# Strongly recommended
version: 1.2.0
argument-hint: "[--audit path/to/file]"
# Pass-throughs
license: MIT
compatibility: "any node runtime"
metadata:
short-description: "Project HTTP API style guide"
owner: platform-team
# Advisory — v1 does not pre-approve tools; see §6
allowed-tools: Read, Grep, Bash(git log:*)
# Visibility
disable-model-invocation: false
user-invocable: true
---
# API Conventions
This project uses RESTful endpoint naming. ...
## When reviewing a new endpoint
1. Does the path use plural nouns? `/users`, not `/user`.
2. Does the response envelope match `references/error-schema.json`?
3. ...
## Scripts
Run `scripts/audit-endpoints.js <path>` to scan an endpoint file for convention
violations. The script prints a report; no context is consumed by the script's
source.
5. Workspace, discovery, and permission model
5.1 Skill roots
A session declares one or more skill roots. Each root is an absolute directory that contains skill subdirectories. Example:
session.skill_roots = [
"/workspace/.skills", // project skills, committed with code
"/home/user/.claude/skills", // user skills, personal
"/home/user/.agents/skills" // shared across harnesses
]
The harness controls which roots are active. The tool scans only these; no walking up to /.
Precedence. Lower-index roots shadow higher-index ones. Project skills (index 0) override user skills (index 1) with the same name. Name collisions are resolved silently; a meta.shadowed: string[] field is populated on the catalog entry so tooling can audit.
5.2 Discovery
At session start (or when the catalog is queried):
- For each root, list immediate subdirectories.
- For each subdirectory, look for
SKILL.md. Skip dirs without one. - Parse frontmatter. Skills with
INVALID_FRONTMATTERare logged but omitted from the catalog. - Build
catalog: { name, description, dir, root_index }[], sorted by name.
Discovery errors are never fatal to session start — a broken skill shouldn't prevent the harness from running. They surface as warnings in the harness log and as INVALID_FRONTMATTER errors when the model attempts to activate by name.
5.3 Workspace fence
Same pattern as read/write/lsp. The resolved skill dir must be inside one of the session's configured skill roots. A skill whose dir resolves outside any configured root — via symlink, .., or session misconfiguration — returns OUTSIDE_WORKSPACE.
5.4 Sensitive patterns
If a skill dir matches any sensitive_patterns entry (e.g. **/.env/**, **/secrets/**) AND no hook is configured, activation is refused with SENSITIVE. Rationale: skill bodies can contain code snippets referencing credentials; a skill under .env/ is almost certainly a misconfiguration.
5.5 Trust gating (autonomous-mode extension)
Autonomous-mode-specific. Session config:
session.skill_trust = {
trusted_roots: string[], // e.g. [user's home skills dir]
untrusted_project_skills: "hook_required" | "warn" | "allow"
}
Semantics:
- Skills under a root listed in
trusted_rootsactivate freely (permission hook still runs as advisory). - Skills under other roots with
untrusted_project_skills: "hook_required"(the default) require explicit hook approval. The hook receives the skill's dir, frontmatter, and areason: "untrusted_project_skill"flag. "warn"allows activation but emits a console warning."allow"matches legacy behavior; no gating.
Skills are code-adjacent (they can call tools via allowed-tools, execute scripts). Cloning a repo and activating its skills without review is a supply-chain risk. This flag lets harness authors enforce a review step.
5.6 Permission hook signature
hook({
tool: "skill",
action: "activate",
path: skill_dir,
always_patterns: ["Skill(name:{name})"],
metadata: {
name,
root_index,
frontmatter: { name, description, version, allowed_tools, ... },
reason: "normal" | "untrusted_project_skill",
}
}) → "allow" | "allow_once" | "deny"
ask → deny, matching autonomous-mode semantics of all other tools in this library. unsafeAllowSkillWithoutHook: true on the session permission policy is the no-hook escape hatch for test fixtures.
6. allowed-tools composition
This is the field that couples skills to the rest of the tool surface. Design decision (§4.3 option 1 in the author's proposal):
allowed-tools is advisory in v1. A skill's allowed-tools: Bash(git:*), Read, Grep does not pre-approve those tools for the skill's lifetime. The session's permission hook is authoritative on every downstream tool call. The field serves three purposes:
- Documentation. The model sees
allowed-toolsin the skill body wrap (§3.1). It can plan tool usage accordingly. - Catalog signaling. Skills that need no tools (pure-instructions skills) can omit the field; skills that need
Bash(git:*)signal that up front. - Audit. The hook receives
allowed-toolsas metadata on activation and on every downstream tool call, so a harness can enforce consistency ("this skill is active; the hook should allow its declared tools automatically").
v1.1 may add a "pre-approve" opt-in mode where activation with explicit hook consent pre-approves the listed tools for the skill's lifetime. Deferred until we have an audit-log surface to record the pre-approvals.
Anti-pattern to block in v1: a skill with allowed-tools: Bash(*) activating to circumvent a session-level deny-Bash policy. The advisory posture makes this structurally impossible.
7. Activation lifecycle
Catalog load (session start or first
skillcall). Walkskill_roots, parse frontmatter, build catalog. Cost: ~50-100 tokens per skill of metadata, stored in the tool's description-level or in a system-prompt injection (harness choice).Model calls
skill({ name, arguments }).Validation. Parse params, verify name is in catalog, check permission hook, check trust gate, check
disable-model-invocation.Body expansion. Read SKILL.md, strip frontmatter, substitute
$ARGUMENTS/$1/$nameplaceholders ifargumentswas provided. Enumerate up to 10 resource filenames fromscripts/,references/,assets/(names only, not contents).Return. The
okresult includes the wrapped<skill>block. The harness is free to cache that the skill has been activated this session.Dedupe. Subsequent calls to
skill({ name: same })returnalready_loaded. The dedupe key is the skill name — not the full content — because the model treats skill identity by name, not by body hash.Session end. Catalog discarded; activated-skill set discarded. No persistence.
Explicit non-behavior: the tool does not watch the filesystem. Editing a skill file mid-session does not invalidate the dedupe cache — authors restart the session to pick up edits. Simpler semantics; avoids surprise when edit-and-re-activate returns a stale body.
8. Resource expansion — scripts/, references/, assets/
Per agentskills.io, a skill folder may contain:
scripts/— executables. Source code is NOT loaded into context. The skill body tells the model "runscripts/audit.py <arg>"; the model callsbash(<dir>/scripts/audit.py <arg>); the script's output (not its source) enters the context.references/— Markdown docs too big for the skill body. The skill body says "seereferences/error-schema.md"; the model callsread(<dir>/references/error-schema.md)when needed.assets/— templates, diagrams, images. Used in output; copied by scripts or read by Read.
The skill tool does not eagerly load any of these. It lists up to 10 filenames per folder in the <resources> block so the model can see what's available, and stops there.
Rationale: this is the efficiency story. A skill with a 200-line script and a 3000-line reference doc costs ~100 tokens in the catalog, ~2000 tokens to activate, and 0 tokens for the script/reference until they're explicitly called. The agentskills.io spec is emphatic: "progressive disclosure is the point."
9. Argument substitution
If the skill body contains one or more of the following, and arguments is provided:
$ARGUMENTS→ the full string form of arguments (if arguments is a string) or the arguments formatted as space-separatedkey=valuepairs (if object).$1,$2, ...,$N→ positional tokens fromargumentssplit on whitespace (string form only).$ARGUMENTS[0],$ARGUMENTS[1], ... → same as$Nbut 0-indexed.${name}— a named argument, substituted fromargumentsobject'snamekey. Only honored if the frontmatter declaresarguments: { name: {type, required, ...} }.
Unsubstituted placeholders are left as literal text (not stripped). If the skill references $1 but no arguments were passed, the model sees $1 in the body and can proceed or retry with arguments.
Deliberately not supported in v1: ${SKILL_DIR} path resolution. Scripts and references are called with full paths the tool emits in the <resources> block; the model doesn't need relative-path resolution.
10. Pluggable adapter
interface SkillRegistry {
// Called once at session start (or lazy on first tool call).
discover(): Promise<readonly SkillEntry[]>;
// Called on every activation. May re-read from disk or serve from cache.
load(name: string): Promise<LoadedSkill | null>;
}
interface SkillEntry {
readonly name: string;
readonly description: string;
readonly dir: string;
readonly root_index: number;
readonly frontmatter: Readonly<Record<string, unknown>>;
}
interface LoadedSkill extends SkillEntry {
readonly body: string; // frontmatter-stripped
readonly resources: readonly string[]; // filenames only
}
Default: FilesystemSkillRegistry(roots: string[]) — the §5 behavior.
Adapters allow skills to come from non-filesystem sources:
@agent-sh/harness-skill-git— skills pulled from a git repo, versioned.@agent-sh/harness-skill-http— skills served from an HTTP endpoint (enterprise skill library).@agent-sh/harness-skill-db— skills stored in a database with audit logging.
Core never depends on adapters. The FilesystemSkillRegistry is the only default.
11. Ledger integration
Skill activation is not a filesystem mutation, so the read-before-edit ledger doesn't apply to skill itself. However:
- Scripts invoked by a skill pass through
bashand its normal permission hook. - References read by a skill pass through
readand record into the read-ledger normally. - Edits made during an active skill pass through
edit/multieditand their normal NOT_READ_THIS_SESSION / STALE_READ checks.
No special ledger exemption for skill-triggered mutations. This keeps the read-before-edit invariant intact. Skills that want to pre-authorize edits must document "read file X before calling edit on it" in their body — same discipline the model already has.
12. Determinism, idempotence, concurrency
- Determinism. Skill body is byte-exact from disk. Frontmatter-strip is deterministic. Argument substitution is deterministic. Given the same SKILL.md + same
arguments,skill({name, arguments})returns the same output within a session. Across sessions it depends on filesystem state. - Idempotence. Explicit via
already_loaded. Second call is a no-op. - Concurrency. Multiple in-flight
skillcalls in the same session are serialized through a per-session lock on the catalog. Parallel activation of different skills is allowed but their bodies are appended to context in call-order, not in tool-response-order, to keep the conversation stable.
13. Tests — acceptance matrix
13.1 Unit (code correctness)
- Empty
name→INVALID_PARAM. namenot lowercase-kebab →INVALID_PARAM.name> 64 chars →INVALID_PARAM.namenot in catalog →NOT_FOUNDwith fuzzy siblings.- Alias pushback (
skill_name,params,reload) →INVALID_PARAM. - SKILL.md with name mismatch (
name: fooinbar/SKILL.md) →NAME_MISMATCH. - SKILL.md with malformed YAML →
INVALID_FRONTMATTER. - SKILL.md with missing required
description→INVALID_FRONTMATTER. - SKILL.md with unknown field
hooks→ passes parse, field preserved infrontmatteroutput. - Skill dir with
../escape →OUTSIDE_WORKSPACE. - Skill dir matching sensitive pattern, no hook →
SENSITIVE. disable-model-invocation: trueactivated from model →DISABLED.disable-model-invocation: trueactivated from user-initiated (harness flag) → ok.- Trust: untrusted project skill with no hook →
NOT_TRUSTED. - Trust: untrusted project skill with
unsafeAllowSkillWithoutHook: true→ ok. - First activation →
kind: "ok", body in output. - Second activation same name →
kind: "already_loaded". - Activation with string
arguments→$ARGUMENTSsubstituted in body. - Activation with object
arguments+ frontmatterargumentsdeclaration →${name}substituted. - Object
argumentson a skill with noargumentsfrontmatter →INVALID_PARAM. - String
argumentson a skill with namedargumentsfrontmatter →INVALID_PARAM. - Resources enumeration: skill with
scripts/a.py,scripts/b.sh,references/r.md→ all listed in output, up to 10. - Resource count > 10 → output lists 10 + "(... {N} more)" marker.
- Permission hook returns
deny→PERMISSION_DENIED. - Permission hook returns
ask→ treated as deny. allowed-toolsstring form ("Read, Grep") → parsed to["Read", "Grep"].allowed-toolsarray form → accepted.- Name collision project > user → catalog shows project skill;
frontmatter.metadata.shadowedlists the shadowed user skill dir. - Broken skill dir (no SKILL.md) → omitted from catalog silently.
- SKILL.md with CRLF line endings → body and frontmatter parse correctly.
13.2 LLM e2e (model-contract validation)
Lives in packages/harness-e2e/test/skill.e2e*.ts. Minimum categories (SK1…SK8):
- SK1 golden: prompt references a skill's described trigger; model calls
skill({name})once, then follows its instructions. - SK2 wrong-name: prompt mentions a skill by paraphrase; model picks from catalog, NOT_FOUND on first try, recovers via siblings.
- SK3 already-loaded: agent activates the same skill twice; second call returns
already_loadedand the model doesn't re-ask for content. - SK4 allowed-tools-advisory: skill declares
allowed-tools: Bash(git:*); model attemptsbash(git log)which the hook permits; but attemptsbash(rm -rf)which the hook denies. Verifies skill declaration does not pre-approve. - SK5 disable-model-invocation: skill has
disable-model-invocation: true; model callsskill({name})and getsDISABLED; does not retry. - SK6 resource-reference: skill body says "run scripts/audit.py"; model calls
bash(<dir>/scripts/audit.py)with the dir from the skill's resource listing. - SK7 argument-passing: skill declares named
arguments; model callsskill({name, arguments: {path: "..."}})with correct shape. - SK8 trust-gate: untrusted project skill, no hook approval; model gets NOT_TRUSTED; does not try to bypass by changing activation shape.
Stochastic ones (SK4 especially) in pass@k.
Multi-model coverage per agent-knowledge/testing-harness-tools.md: gemma4:e2b + gemma4:26b + qwen3:8b + qwen3.5:27b via Ollama; per-release Bedrock Opus 4.7.
14. Stability
Breaking changes bump major. Additions (new error codes, new optional frontmatter fields, new params) are minor. Error code values are a public contract. Frontmatter field names are a public contract — renaming description → desc is breaking.
Forward-compatibility stance: the parser ignores unknown frontmatter fields, so authors can target future Claude Code / OpenCode extensions without our parser rejecting their file.
15. Open questions (deferred)
context: fork— subagent-isolated skills. Requires a subagent primitive we don't ship. When we do, this is the first v1.1 addition. The design question is the context-forking semantics: does the parent see the fork's tool calls? Does the fork inherit the parent's read-ledger?- Dynamic shell injection (
!`<cmd>`in the body). Killer ergonomic feature. Security surface: a skill body canrm -rfon activation. Defer until we have a sandbox layer or a per-block confirmation flow. - Live filesystem watching. Nice but not load-bearing. Skill authors currently restart sessions to pick up edits; that's fine.
pathsauto-activation gating. Needs a file-context awareness layer we don't have. Could be added later as a session-level filter on the catalog.allowed-toolspre-approval mode. See §6. Wait for evidence that the advisory mode is insufficient before adding an audited pre-approve option.- Skill composition. Can a skill's body reference
{{skill:other-skill}}to pull in another skill's body at expansion time? Spec says no; model can callskillagain if it needs both. Watch for author demand. - Skill argument types beyond strings. Named arguments in v1 are strings only. Frontmatter
arguments: { count: { type: "integer" } }would let the harness coerce. Defer. - Project-trust UX. The trust gate (§5.5) is the right safety posture, but the UX of "a new skill appeared in your project, approve it?" is a harness concern. Our design leaves that to the hook.
- MCP cross-exposure. Skills as MCP prompts —
@agent-sh/skill-mcp-bridgecould expose installed skills as MCP prompts to non-Claude harnesses. Secondary distribution; not core.
16. References
agent-knowledge/skill-tool-design-across-harnesses.md— the 14-section deep dive across Claude Code, OpenCode, Gemini, Continue, Codex, Amp, and 35+ adopters (primary).agent-knowledge/skill-tool-in-autonomous-agents.md— complementary research on autonomous-agent skill patterns: OpenHands, Hermes, SWE-agent, Voyager lineage, Letta, OpenAI Agents SDK primitives.agent-knowledge/harness-tool-surface-audit.md§Skill — the gap analysis that motivated the ship decision.agent-knowledge/ai-agent-harness-tooling.md§Progressive disclosure — the architectural argument.- Anthropic "Agent Skills" announcement — Oct 16 2025 engineering blog.
agentskills.io/specification— the required-vs-optional frontmatter table that §4 mirrors.- Claude Code
Skilldocs —code.claude.com/docs/en/skills(tool description, frontmatter extensions). - OpenCode
skill.ts—packages/opencode/src/tool/skill.ts(the resource-enumeration pattern). - Simon Willison's "Claude Skills are awesome, maybe a bigger deal than MCP" — the token-economics argument.
anthropics/skillsrepository — Apache 2.0 reference skills (structure and style reference).
Addendum: decision log
- S-D1 (Tool name):
skill(lowercase). Matches OpenCode / Gemini CLI convention. Claude Code usesSkill(uppercase); we match the emerging consensus. - S-D2 (Activation pattern): Pattern A — dedicated tool. Alternatives (file-read + catalog-in-system-prompt, slash-command-only) considered and rejected on tool-surface-count grounds per
agent-tool-use-methods.md§tool-count limits. A dedicated tool gives us the dedupe, permission-gate, and structured-wrap hooks we need; the extra tool cost is 1 schema in the surface. - S-D3 (Frontmatter scope): agentskills.io required + optional fields + 4 Claude Code extensions widely adopted in our corpus (
version,argument-hint,disable-model-invocation,user-invocable). Other Claude Code extensions deferred to v1.1. - S-D4 (
allowed-toolssemantics): advisory in v1. Matches our autonomous-mode posture (no implicit privilege grants). Pre-approve mode deferred to v1.1 with audit logging. - S-D5 (Trust gating): autonomous-specific extension. Untrusted project skills default to hook-required. Matches the pattern set by
unsafeAllowLspWithoutHookon LSP. - S-D6 (Dedupe by name): identity is skill name, not body hash. Edit-and-re-activate does not bust the cache; session-restart does. Simpler mental model.
- S-D7 (Resource expansion): names only, max 10 per folder. Scripts and references load on demand via bash/read. Matches OpenCode behavior.
- S-D8 (Argument substitution):
$ARGUMENTS/$N/$name. v1 strings only; named-argument typing deferred. - S-D9 (Ledger): skill doesn't mutate, so no ledger interaction. Downstream tools still use their normal ledger semantics.
- S-D10 (Permission): hook receives skill dir + frontmatter on activation. Follows the fail-closed-without-hook pattern of bash / webfetch / lsp.
unsafeAllowSkillWithoutHookis the test-fixture escape. - S-D11 (Pluggable registry):
SkillRegistryinterface withFilesystemSkillRegistrydefault. MirrorsSandboxAdapter(bash),WebFetchEngine(webfetch),LspClient(lsp) plugin boundaries. - S-D12 (Forward compat): unknown frontmatter fields preserved, not rejected. Authors target Claude Code's newer extensions without our parser breaking.