# SKILL.md format

A `SKILL.md` file is a YAML frontmatter block followed by a markdown body. The framework's parser (`SkillMdParser`) accepts the same shape Claude Code's parser does, so a skill authored once works in both environments.

## File layout

`<root>/<skill-name>/SKILL.md` — the directory name is the default skill name when the frontmatter is silent on `name`. `FileSystemSkillStore` scans `<root>/` and treats every immediate child directory containing a `SKILL.md` as a registered skill.

```
.tnsai/skills/
├── deploy/
│   ├── SKILL.md
│   ├── reference.md            (optional, lazy-referenced by body)
│   └── scripts/precheck.sh     (optional, executed by skill body)
├── lint/
│   └── SKILL.md
└── customer-escalation/
    └── SKILL.md
```

Supporting files (`reference.md`, scripts, …) are NOT loaded eagerly — the body declares them by relative path and consumers fetch on demand.

## Frontmatter reference

```yaml
---
name: deploy                          # falls back to parent directory name
description: Production deploy procedure with rollback support
when-to-use: |
  When the user asks to ship a build to prod or staging.
  Trigger phrases: "deploy", "ship", "release", "rollout".
allowed-tools:
  - bash
  - kubectl
  - github.create_pr
argument-hint: "<environment>"
arguments:
  - environment
disable-model-invocation: false       # default false
user-invocable: true                  # default true
paths:
  - "infra/**"                        # v2 (file-context auto-activation)
---
```

| Field | Type | Default | Meaning |
|---|---|---|---|
| `name` | string | parent dir | Stable identifier; case-insensitive on lookup. |
| `description` | string | **required** | Always in the system prompt; the resolver scores against it. |
| `when-to-use` | string | empty | Optional context for the resolver — trigger phrases, example requests. |
| `allowed-tools` | list of strings | empty | Tools auto-granted while this skill is active. Empty = no constraint. |
| `argument-hint` | string | empty | Free-form hint shown in catalog listings. |
| `arguments` | list of strings | empty | Positional argument names for `argument-hint`. |
| `disable-model-invocation` | boolean | `false` | If `true`, the LLM may not activate this skill — only the user (or `Agent.invokeSkill(...)`) can. |
| `user-invocable` | boolean | `true` | If `false`, hidden from the user-facing menu and rejected on `/skill-name`. Programmatic activation still works. |
| `paths` | list of glob patterns | empty | v2 only: auto-activate when the agent is operating on matching files. |

### Field aliases

The parser accepts both the kebab-case (`agentskills.io` standard) and snake_case (Claude Code docs) forms for fields where the docs are split:

| Canonical | Aliases |
|---|---|
| `when-to-use` | `when_to_use` |
| `allowed-tools` | `allowed_tools` |
| `argument-hint` | `argument_hint` |

### List shorthand

A scalar in place of a single-element list is accepted for `allowed-tools`, `paths`, and `arguments`:

```yaml
allowed-tools: bash       # equivalent to: ["bash"]
```

### CRLF tolerance

Files written on Windows / via `git autocrlf=true` parse identically — the parser normalises line endings before extracting the frontmatter.

## Body

Anything between the closing `---` delimiter and the end of the file is the skill body. The body is plain markdown — the framework does not parse it.

The body is rendered through `SkillSubstitution` before it lands in the system prompt, so placeholders are resolved at activation time (not at parse time).

## Substitution

| Placeholder | Resolves to |
|---|---|
| `$ARGUMENTS` | All positional arguments space-joined. |
| `$0`, `$1`, … | The Nth positional argument (0-indexed). Out-of-range = empty string. |
| `${VAR}` | Named env value supplied by the caller. Unknown name = empty string. |
| `$$` | Literal dollar sign — escapes the placeholder at that position. |

Substitution is **single-pass and left-to-right** — placeholder values are not re-substituted. A literal `$ARGUMENTS` passed as `$0` stays literal in the output.

```markdown
---
name: deploy
description: Deploy a build to a target environment
arguments:
  - environment
---
# Deploy to $0

1. Verify CI is green for the build tagged `${BUILD_TAG}`.
2. `kubectl apply -f manifests/$0/`
3. Smoke-test https://$0.example.com/healthz.
```

Invoked as `/deploy staging` with `env={BUILD_TAG: "v1.4.2"}`, the body renders as:

```markdown
# Deploy to staging

1. Verify CI is green for the build tagged `v1.4.2`.
2. `kubectl apply -f manifests/staging/`
3. Smoke-test https://staging.example.com/healthz.
```

## Validation rules

The parser rejects:

- Files without an opening `---` delimiter on the first line.
- Files with an opening delimiter but no closing one.
- Frontmatter that is not valid YAML.
- A `Skill` with blank `name` (after defaulting from the directory name) — every skill must have a name.
- A `Skill` with blank `description` — the resolver depends on it.

A malformed `SKILL.md` is **logged and skipped** by `FileSystemSkillStore`; sibling skills continue to load. One broken skill on disk does not poison the store.

## Authoring tips

- **Lead the description with the problem the skill solves**, not the solution. The resolver scores by token overlap with the user's message — "deploy build" lands closer to "ship build" than "ship a release". Name the user's intent in your own words.
- **Use `when-to-use` for trigger phrases**. The keyword resolver weights `when-to-use` matches 2× and `name` matches 3×; trigger phrases here move the skill ahead of competitors.
- **Keep the body procedural, not narrative**. Numbered steps + concrete commands. The body is what the LLM follows once activated; flowery prose dilutes the signal.
- **Reference supporting files by relative path**. The body can say "see `reference.md` for examples" — the consumer (LLM or human) fetches on demand instead of bloating the activation snapshot.

## See also

- [Skills overview](index.md) — when to reach for skills vs tools / RAG / capabilities
- [Registration](registration.md) — wiring a store + resolver into `AgentBuilder`
