# 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, such as trigger phrases and 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, because 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`, and 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, so "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×, so 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, and flowery prose dilutes the signal.
- **Reference supporting files by relative path**. The body can say "see `reference.md` for examples", and 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](add.md): wiring a store + resolver into `AgentBuilder`
