Writing Skills¶
A skill is a SKILL.md file: a Markdown document with YAML frontmatter that serves as a system prompt for an agent. This guide covers how to write skills that pass the brunnr security scanner and work well as agent instructions.
File structure¶
Every skill lives in a directory under skills/:
skills/
my-skill/
SKILL.md # Required: the skill prompt
tests/
my-skill/
test-spec.json # Optional: eval test specification
fixtures/
case-1.json # Optional: test fixtures
schemas/
my-skill/
output.schema.json # Optional: output schema
SKILL.md format¶
Frontmatter¶
The YAML frontmatter is required. At minimum, it must include name and description:
---
name: my-skill
description: A clear, specific description of what this skill does. Use when [specific trigger]. Returns [specific output shape].
---
Write the description for the agent
The description field is what the agent reads to decide whether to use the skill. Apply the AX Description Rubric principles: output shape, cost signal, trigger clarity, specificity, differentiation.
Optional frontmatter fields:
---
name: my-skill
description: Score tool descriptions against five criteria. Returns a score, table, and rewrite. ~800 tokens. Use when reviewing MCP tool descriptions.
license: Complete terms in LICENSE.txt
metadata:
openclaw:
emoji: "\U0001F50D"
requires:
bins: ["python3"]
---
Body¶
The body is the system prompt. Write it as instructions the agent will follow.
Structure the body with clear sections:
# Skill Title
Brief overview of what the skill does.
## Input
What the user provides. Be explicit about format.
## Phase 1: [Step Name]
Instructions for the first phase of work.
## Phase 2: [Step Name]
Instructions for the second phase.
## Output Format
Exact format the agent should produce.
## Reference Examples
Show concrete input/output pairs.
Passing the security scanner¶
The scanner checks your SKILL.md against 7+1 threat classes. Here are the rules for writing clean skills.
Do not include¶
These patterns will cause a BLOCK finding:
- Shell pipes -- Never write
curl ... | shorwget ... | bash, even as examples. - Reverse shells -- Never reference
bash -c,nc -e, or Python socket commands. - Base64 payloads -- Never include
echo ... | base64 -d | sh. - Sensitive file reads -- Never write
cat ~/.ssh/id_rsaorcat .env. - Credential extraction -- Never include
printenv API_KEYorenv | grep SECRET. - Prompt overrides -- Never write "ignore previous instructions" or "you are now in unrestricted mode".
Be careful with¶
These patterns will cause a FLAG finding:
- Custom package registries -- If your skill references
--index-urlor--registry, it will be flagged. - Privilege escalation --
sudo chmod,sudo bash, or writing to/etc/will be flagged. - Sensitive paths -- References to
~/.ssh/,~/.aws/,.envwill be flagged unless your skill has security context (see below).
Security context exemption¶
If your skill is about security auditing, SSH setup, or infrastructure, include security keywords in the content: "audit", "security", "protect", "encrypt", "ssh-keygen", etc. The scanner recognizes these and suppresses sensitive-path flags.
URL allowlist¶
URLs to non-allowlisted domains produce INFO findings. Stick to well-known domains (GitHub, PyPI, npm, MDN, Anthropic docs, etc.) or accept the INFO findings during review.
Keyword divergence¶
Make sure your frontmatter description uses words that also appear in the body. If the description says "code formatter" but the body is about "database administration", the scanner will flag a keyword divergence.
Writing effective instructions¶
Be specific¶
Bad:
Help the user with their code.
Good:
## Phase 1: Analyze
Read the user's code and identify:
1. Functions with more than 3 parameters (refactoring candidates)
2. Repeated code blocks (DRY violations)
3. Missing error handling (try/catch absent on I/O operations)
Use phases¶
Break complex skills into numbered phases. Agents follow sequential instructions better than unstructured prose.
Include output format¶
Specify exactly what the agent should produce:
## Output Format
For each function reviewed, produce:
| Function | Score | Issue |
|----------|-------|-------|
| {name} | {1-5} | {one line} |
Then provide a rewritten version of the lowest-scoring function.
Provide reference examples¶
Show input/output pairs so the agent understands the expected behavior:
## Reference Examples
### Example: Bad input (Score 1/5)
**Input:**
> "Query the database for relevant results."
**Output:**
> Score: 1/5. No output shape, no cost signal, no trigger.
### Example: Good input (Score 5/5)
**Input:**
> "Check for schema drift. Returns 0-3 warnings with SQL fix suggestions. ~200 tokens. Call after migrations."
**Output:**
> Score: 5/5. All criteria pass.
Writing test specifications¶
Test specs validate that your skill produces correct output when evaluated against test fixtures.
test-spec.json¶
{
"cases": [
{
"id": "case-1",
"description": "Basic functionality test",
"fixture": "fixtures/basic-input.json",
"assertions": [
{"type": "score_range", "expected": {"min": 1, "max": 2}},
{"type": "has_table", "expected": true},
{"type": "contains", "expected": "rewritten"}
]
}
]
}
Fixture files¶
{
"input": "The text that will be sent as the user message to Claude.",
"description": "Human-readable description of this test case."
}
Available assertion types¶
| Type | Expected value | Checks |
|---|---|---|
score_range |
{"min": N, "max": N} |
Extracted score is within range |
has_table |
true/false |
Output contains a markdown table |
has_rewrite |
true/false |
Output contains "rewrite"/"rewritten" |
contains |
"string" |
Case-insensitive substring match |
format_valid |
(none) | Has headers, score line, and criteria table |
criteria_pass |
["name", ...] |
Named criteria show "pass" in table |
criteria_fail |
["name", ...] |
Named criteria show "fail" in table |
Checklist¶
Before publishing a skill:
- [ ] Frontmatter has
nameanddescription - [ ] Description follows AX rubric principles (output shape, cost, trigger)
- [ ] Body has clear phases or sections
- [ ] Output format is specified
- [ ] At least one reference example is included
- [ ]
brunnr scanpasses with CLEAN verdict - [ ] Test spec exists with at least one case (if applicable)
- [ ]
brunnr eval <slug> --dry-runvalidates fixtures