Agent Skills
Package reusable know-how as Skills Claude loads on demand: SKILL.md, triggering descriptions, progressive disclosure (with a cost model), bundled helpers, distribution, and an org Skills library.
Learning objectives
- Explain what an Agent Skill is and when it beats a prompt or CLAUDE.md.
- Write a SKILL.md with a triggering description and bundled files.
- Understand progressive disclosure and why it keeps Skills cheap.
- Distribute and govern Skills across a team.
code/ak4-agent-skills/. Python runs offline; configs are ready to use.1 · What a Skill is essential
An Agent Skill is a folder with a SKILL.md that packages reusable know-how — instructions, and optionally scripts and reference files — that Claude loads on demand when a task matches. It turns "the way we do X" into a versioned, shareable capability.
This picture shows the life of a request when Skills are installed — and the one idea that makes Skills cheap: Claude does not read every Skill up front. It reads only a tiny label for each, and opens the full Skill only when your task actually matches it.
- Box 1 · Task arrives — you send a normal request ("write me an incident report").
- Box 2 · name+desc match? — for each installed Skill, Claude compares your task against just the Skill's
nameand one-linedescription. The caption calls this a "cheap check" because those labels are tiny (a few words), so checking them costs almost nothing even with dozens of Skills. - Box 3 · load SKILL.md body — only on a match does Claude read the full instructions. A Skill that doesn't match is never opened, so you don't pay for it.
- Box 4 · use bundled files — once loaded, the Skill can pull in its extra files (templates, scripts) as the instructions call for them.
- Read it left to right: each arrow is "next step". The flow narrows from "look at every Skill's label" down to "fully load just the one that fits".
In short: "Progressive disclosure" = reveal detail only when needed. Labels are always visible (cheap); the heavy body appears just-in-time. That is why a team can install many Skills without slowing every chat.
2 · Anatomy of a Skill essential
layout.txt.claude/skills/
└── incident-report/
├── SKILL.md # instructions + trigger metadata
├── template.md # a reference the body points to
└── scripts/
└── gather_logs.sh # an optional helper the Skill can run
A Skill is nothing exotic — it is just a folder on disk with a specific shape. This is a directory tree (the └── and ├── lines are branches, like a family tree of files). Reading it tells you exactly what files to create.
.claude/skills/is where Claude looks for Skills. Each Skill gets its own sub-folder — hereincident-report/, whose name becomes the Skill's name.SKILL.mdis the required file: the instructions plus the trigger metadata (covered next). Every Skill must have exactly one.template.mdis an optional reference file — extra material the instructions can point to (a house format, a checklist). It loads only if the body asks for it.scripts/gather_logs.shis an optional helper script the Skill can run. The#text after each file is just a comment explaining its purpose, not part of the name.
Try this: Recreate this tree yourself: mkdir -p .claude/skills/incident-report/scripts and add an empty SKILL.md. That empty folder is already a (do-nothing) Skill.
SKILL.md---
name: incident-report
description: >
Write a postmortem/incident report in our house format. Use whenever the user
asks for an incident report, postmortem, or RCA writeup.
---
# Incident report
1. Use the structure in `template.md` (timeline, impact, root cause, actions).
2. If logs exist, run `scripts/gather_logs.sh <service>`.
3. Timeline entries are UTC, newest last; every claim cites a log line or ticket.
4. "Action items" need an owner and a due date each.
5. No blame language — describe systems and events, not people.
This is the heart of a Skill: the SKILL.md file. It has two parts — a small metadata header (between the --- lines) that tells Claude when to use the Skill, and a Markdown body that tells Claude how to do the task.
- The block fenced by
---at top and bottom is YAML front-matter (a tiny key: value settings header).nameidentifies the Skill;descriptionis the all-important trigger line. - The
description: >uses a>so the text can wrap onto several indented lines and still count as one value. Notice it says "Use whenever the user asks for an incident report, postmortem, or RCA..." — it names the exact words a user would say, which is what makes Claude fire the Skill. - Everything below the closing
---is the body: ordinary Markdown instructions. The# Incident reportis a heading; the numbered list is the step-by-step procedure Claude follows. - The body can reference the bundled files: step 1 points at
template.mdand step 2 runsscripts/gather_logs.sh— the same files from the folder tree above. The rules (UTC timeline, cite every claim, owners on action items, no blame language) capture "how we do it" once, reusably.
What the output means: There is no program output here — SKILL.md is instructions, not code. Its effect is that asking Claude for a postmortem now produces one in exactly this house format.
Try this: Rewrite the description to be vague ("Writes reports") and you'll see the problem the lesson warns about — a vague description often never triggers. Keep the concrete "use whenever..." phrasing.
name + description alone. Write the description around when to use it ("use whenever the user asks for X") — vague descriptions never fire; precise ones do.3 · Why progressive disclosure matters intermediate
Only each Skill's name + description are always in context (tiny). The full body loads only on a match. That's what lets a team ship dozens of Skills without every session paying for all of them — the context-economy lesson from PE2.
disclosure_cost.pydef context_cost(skills, matched):
"""Always-loaded: name+desc (~20 tokens each). Body (~500) only if matched."""
always = sum(20 for _ in skills)
bodies = sum(500 for s in skills if s in matched)
return always + bodies
skills = ["incident-report", "release-notes", "code-review", "data-clean", "sql-style"]
print("all 5 skills, none matched:", context_cost(skills, set()), "tokens (just metadata)")
print("all 5, one matched: ", context_cost(skills, {"incident-report"}), "tokens")
print("if bodies always loaded: ", 5*520, "tokens (5x waste)")
all 5 skills, none matched: 100 tokens (just metadata)
all 5, one matched: 600 tokens
if bodies always loaded: 2600 tokens (5x waste)
This tiny program puts numbers on why progressive disclosure (the diagram above) saves money. It's a made-up but realistic cost model: it counts context tokens — the units Claude reads and you pay for — under different scenarios.
context_cost(skills, matched)is a function taking a list of Skill names and the set of ones that matched this task. The docstring states the assumption: each Skill's name+desc costs ~20 tokens (always loaded), each full body ~500 tokens (loaded only on a match).always = sum(20 for _ in skills)adds 20 for every Skill — the cheap labels that are always present.bodies = sum(500 for s in skills if s in matched)adds 500 only for the matched ones. The total isalways + bodies.- The three
printlines run three what-ifs: 5 Skills with none matched (labels only), 5 Skills with one matched, and the wasteful world where all 5 bodies load every time (5*520).
What the output means: none matched: 100 tokens (just 5×20 labels); one matched: 600 (100 labels + one 500 body); always loaded: 2600. The point: 100 vs 2600 — loading bodies only on demand is roughly a 5× saving.
Try this: Change matched to two Skills, e.g. {"incident-report","code-review"}, and predict the number before running (100 + 2×500 = 1100). More matches cost more — but you still skip the Skills that didn't match.
4 · Advanced — a Skill that bundles a script advanced
Skills can bundle runnable helpers the instructions invoke. Keep the helper a clean, testable unit (DF1). Here's a helper a Skill might ship, runnable on its own.
skill_helper.py# scripts/summarize_incidents.py — a helper the incident-report Skill calls
def summarize(events):
"""events: [(ts, service, severity)] -> a timeline + a headline count."""
events = sorted(events) # chronological
crit = sum(1 for _,_,sev in events if sev == "critical")
timeline = [f"{ts} UTC — {svc} ({sev})" for ts, svc, sev in events]
return {"critical_count": crit, "timeline": timeline}
r = summarize([("10:05","api","critical"), ("10:01","db","warning"), ("10:12","api","critical")])
print("criticals:", r["critical_count"])
for line in r["timeline"]: print(" ", line)
criticals: 2
10:01 UTC — db (warning)
10:05 UTC — api (critical)
10:12 UTC — api (critical)
This is an example of the bundled helper script a Skill can ship (the scripts/ file from the folder tree). The point is that a Skill isn't limited to prose — it can carry real, runnable code that does the fiddly work reliably every time.
summarize(events)takes a list of incident events. The docstring shows the shape: each event is a tuple(ts, service, severity)— a timestamp, a service name, and how bad it was.events = sorted(events)puts them in chronological order (sorting tuples sorts by the first item, the timestamp). This guarantees a correct timeline no matter what order they arrived.crit = sum(1 for _,_,sev in events if sev == "critical")counts how many werecriticalby adding 1 for each match — the headline count. Thetimelineline builds a readable string per event with an f-string.- It
returns a dictionary with both results. The code below the function calls it on three sample events and prints the critical count, then each timeline line.
What the output means: criticals: 2 (two of the three events were critical), followed by the three events printed in time order — 10:01 before 10:05 before 10:12 — proving the sorted() worked even though 10:01 was listed second in the input.
Try this: Add a fourth event like ("10:08","db","critical") to the list and re-run: the count becomes 3 and the new line slots into the right chronological spot automatically.
5 · Distributing Skills advanced
Skills live at three scopes: project (.claude/skills/, committed — whole team gets them), personal (your user config, across all projects), and plugins (bundled, shared widely). Committing a Skill is how "our way" becomes the team default.
| Scope | Location | Who gets it |
|---|---|---|
| Project | .claude/skills/ (in the repo) | everyone on the repo |
| Personal | your user Claude config | you, everywhere |
| Plugin | a shared plugin bundle | anyone who installs it |
6 · Professional — write a triggering description professional
The most common Skill failure is a description that never fires. Make it specific and action-oriented. Model what a good trigger looks like.
desc_score.pydef score_description(desc):
d = desc.lower(); score, notes = 0, []
if "use when" in d or "use whenever" in d: score += 1
else: notes.append("say WHEN to use it (\"use whenever...\")")
if len(desc) >= 40: score += 1
else: notes.append("too short — name concrete triggers")
if any(w in d for w in ["report","review","summary","writeup","clean","format"]):
score += 1
else: notes.append("name the task nouns users will say")
return score, notes
print(score_description("Writes stuff"))
print(score_description("Write an incident report/postmortem. Use whenever the user asks for an RCA or postmortem writeup."))
(0, ['say WHEN to use it ("use whenever...")', 'too short — name concrete triggers', 'name the task nouns users will say'])
(3, [])
The lesson's biggest warning is that a Skill with a weak description never triggers. This little program is a linter for descriptions — it scores one out of 3 and tells you what's missing, so you can fix a trigger before shipping it.
d = desc.lower()makes a lowercase copy so the checks are case-insensitive.score, notes = 0, []starts the tally at 0 with an empty list of complaints.- Each
ifawards one point for a good trait: it contains "use when/whenever" (says when to fire); it's at least 40 characters (specific, not a stub); and it mentions a concrete task noun (report, review, summary...). When a check fails, theelseappends a note explaining the fix. - It
returns the pair(score, notes). The twoprintlines score a deliberately bad description ("Writes stuff") and a good one.
What the output means: (0, [...three complaints...]) for "Writes stuff" — it earns no points and gets told exactly what to add. The concrete one scores (3, []): full marks, no complaints. The exercise asks you to reach a score of at least 2.
Try this: Paste your own draft description into a third print(score_description(...)) and aim for 3. Adding the words "use whenever the user asks for a ... report" usually flips all three checks to pass at once.
7 · Tech-lead — a Skills library for the org tech-lead
A lead builds a curated Skills library: the team's recurring tasks (reviews, reports, release notes, data routines) each as a versioned Skill, reviewed like code, with clear triggers. New hires inherit the org's know-how automatically.
Exercise AK4.1 — Author & score a Skill
Context: Authoring a real skill forces the two things that make skills work: a description specific enough to trigger reliably, and a bundled helper that does deterministic work the model can reason over.
Your task: Author a SKILL.md for a recurring task with a precise trigger (score it ≥2 with desc_score), bundle one reference file and one runnable helper, and confirm it fires only when it should.
Requirements:
- The skill targets a recurring task you already do a particular way
- The
descriptionis a precise trigger — it scores ≥2 withdesc_score - It bundles one reference file and one runnable helper script, and the helper is tested
- Confirm the skill fires when you ask for that task
- Confirm it stays dormant on unrelated tasks
💡 Hint: Test both directions — that it triggers on the target task and that it stays quiet otherwise — a skill that always fires is as broken as one that never does.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: A Skill is just a folder with a SKILL.md: frontmatter that says what it is and when to load it, plus a body of guidance — the minimum viable unit of reusable know-how.
Your task: Write a minimal SKILL.md for a skill that formats commit messages, including its name and description frontmatter.
Requirements:
- The skill is a folder containing a
SKILL.md(e.g..claude/skills/…/SKILL.md) - Frontmatter includes a
nameand adescription - The
descriptionsays when to load the skill, not just what it does - The body holds the actual guidance (the commit-message format rules)
- Verify the exact frontmatter keys and folder location in the docs
💡 Hint: Name + description + body is the whole minimum — the description is what tells Claude to reach for it.
Show solution
The whole skill is a folder plus a SKILL.md with frontmatter + body:
# .claude/skills/commit-style/SKILL.md
---
name: commit-style
description: Write conventional-commit messages. Use when composing a git
commit message from a diff.
---
Format: `type(scope): summary` where type is feat|fix|docs|refactor|test.
- Summary in imperative mood, <=72 chars, no trailing period.
- Add a body only when the "why" isn't obvious from the summary.
The name identifies the skill and the description tells Claude when to load it; the body is the actual guidance. That is the minimum viable skill. Verify the exact frontmatter keys and folder location in the docs.
Context: Progressive disclosure is how a large skill library stays cheap: only the one-line description sits in context by default, and detail loads on demand.
Your task: Explain the progressive-disclosure tiers and model which parts of a skill are in context at each stage.
Requirements:
- Tier 0 (always): only the skill's
descriptionline is resident - Tier 1 (on use): the full
SKILL.mdbody loads when the task matches - Tier 2 (on demand): bundled files/scripts load only when the body references them
- Model the context cost per stage (idle vs triggered vs deep)
- Explain the payoff: you pay for detail on demand, not up front
💡 Hint: Three stages, growing context: description only, then + body, then + referenced files.
Show solution
Progressive disclosure keeps only the description resident until the skill is needed:
Tier 0 (always): the skill's `description` line only -> cheap, always in context
Tier 1 (on use): the full SKILL.md body -> loaded when relevant
Tier 2 (on demand): bundled files/scripts the body points to -> read as needed
def context_cost(stage):
return {"idle": "description only",
"triggered": "description + SKILL.md body",
"deep": "description + body + referenced files"}[stage]
print(context_cost("idle")) # description only
print(context_cost("triggered")) # description + SKILL.md body
Only the one-line description sits in context by default; the body loads when the task matches, and bundled files load only when the body references them. That is how a large skill library stays cheap — you pay for detail on demand, not up front.
Context: Bundling a script beside a skill lets it do deterministic work — parsing, stats — instead of asking the model to eyeball it, and the script costs nothing until the body references it (Tier 2).
Your task: Sketch a skill folder that bundles a Python helper script and references it from SKILL.md.
Requirements:
- The folder holds
SKILL.mdplus a bundled script (e.g. underscripts/) - The
SKILL.mdbody tells the model to run the script and how to invoke it - The script does deterministic work; the model reasons over its reliable output
- The script loads only when the body references it (Tier 2), so it's free until used
- Verify path conventions in the docs
💡 Hint: Let the script do the exact computation and let the model narrate its output — point to it from the body so it stays lazy-loaded.
Show solution
Bundle the script beside SKILL.md and point to it from the body:
.claude/skills/csv-report/
SKILL.md
scripts/summarize.py <- bundled helper
# SKILL.md body excerpt:
To summarize a CSV, run the bundled script:
python scripts/summarize.py <path-to-csv>
It prints row count, column types, and per-column null rates.
Read its output, then write the narrative summary.
Bundling code lets the skill do deterministic work (parsing, stats) instead of asking the model to eyeball it — the model runs the script and reasons over reliable output. The script loads only when the body references it (Tier 2), so it costs nothing until used. Verify path conventions in the docs.
Context: Because the description is the only part always in context, it is the entire trigger signal — a skill that names the situation fires precisely, while a vague one sits unused or fires on the wrong task.
Your task: Write a strong versus weak skill description and explain why the strong one triggers correctly.
Requirements:
- The weak version is vague about capability and gives no situational cue
- The strong version names concrete triggering situations (what the user asks for)
- It prescribes when to use the skill, not only what it does
- Explain that the description is the only always-in-context part, so it is the whole trigger
- Tie the specificity to firing at the right moment and not on the wrong task
💡 Hint: Describe the user's situation the skill answers ("when the user asks to…"), not the feature list.
Show solution
The description must name the situation, not just the capability:
Weak: description: "Helps with spreadsheets."
-> too vague; the model can't tell when it applies.
Strong: description: "Generate .xlsx spreadsheets with formulas and charts.
Use when the user asks to create, edit, or export an Excel file
or a report as a spreadsheet."
-> names concrete triggers (create/edit/export .xlsx), so the
model loads it exactly when relevant.
Because the description is the only part always in context, it is the entire trigger signal. Prescribe when to use the skill (the user-facing situations) — not just what it does — so it fires precisely and doesn't sit unused or fire on the wrong task.
Context: A distributed skill runs with your agent's permissions, so vetting its bundled scripts is a security step, not a formality; versioning and pinning stop an upstream change from silently altering everyone's behavior.
Your task: Decide the packaging, versioning, and pre-install checks for sharing a skill across the org.
Requirements:
- The skill is a self-contained folder (
SKILL.md+ any scripts) - The description is specific enough to trigger correctly
- Bundled scripts are reviewed like code — they run with the agent's permissions
- No secrets and no calls to untrusted hosts inside the scripts
- Versioned in a shared repo/registry and pinned on install
- Ships with a short README: what it does, when it triggers, what it can touch
- Verify the current distribution/registry mechanism in the docs
💡 Hint: Treat it like importing a dependency that runs as you — review the code, pin the version, document the blast radius.
Show solution
Package it as a versioned folder and review it like code:
Distribution checklist:
[ ] Skill is a self-contained folder (SKILL.md + any scripts).
[ ] Description is specific enough to trigger correctly (see expert rung).
[ ] Bundled scripts are reviewed — they run with the agent's permissions.
[ ] No secrets, no network calls to untrusted hosts inside scripts.
[ ] Versioned in a shared repo / registry; pin a version when installing.
[ ] A short README: what it does, when it triggers, what it can touch.
A distributed skill runs with your agent's permissions, so vetting the bundled scripts is a security step, not a formality. Version and pin it so an upstream change can't silently alter behavior for everyone. Verify the current distribution/registry mechanism in the docs.
Context: A skill earns its place when the know-how recurs and is clearly triggerable; the library's health depends on non-overlapping descriptions so two skills don't fight to trigger, plus single ownership and review.
Your task: As tech lead, write the decision rule for what becomes a skill and the governance policy for an org Skills library.
Requirements:
- The decision rule keys on: repeated, situation-specific, and whether it needs a deterministic step
- One-off work is not a skill — a direct instruction is cheaper
- Recurring + clearly-triggered → yes (bundle a script if deterministic work is needed)
- Borderline cases may be a slash command or a
CLAUDE.mdline instead of a full skill - Governance: one owner per skill, non-overlapping descriptions, review + version in a shared repo, deprecate skills that stop triggering
💡 Hint: Two tests decide it: does the know-how recur, and can you write a description that won't collide with another skill's trigger?
Show solution
Make skills for repeated, situation-specific know-how; govern to avoid sprawl:
def is_skill_worthy(repeated, situation_specific, needs_deterministic_step):
if not repeated:
return "NO — one-off; a direct instruction is cheaper than a skill"
if situation_specific or needs_deterministic_step:
return "YES — recurring, clearly-triggered; bundle a script if it "\
"needs deterministic work"
return "MAYBE — could be a slash command instead of a full skill"
print(is_skill_worthy(True, True, True)) # YES
print(is_skill_worthy(False, True, False)) # NO
# Governance: one owner per skill; non-overlapping descriptions (so two
# skills don't fight to trigger); review + version in a shared repo;
# deprecate skills that stop triggering.
A skill earns its place when the know-how recurs and is clearly triggerable; otherwise a slash command or a line in CLAUDE.md is lighter. The library's health depends on non-overlapping descriptions (so triggering stays predictable), single ownership, and review — the same discipline as a shared code library.
✓ Checkpoint — you can move on when you can…
- Explain when a Skill beats a prompt/CLAUDE.md.
- Write a valid SKILL.md with a triggering description.
- Explain progressive disclosure and its cost model.
- Distribute Skills and curate an org library.
Knowledge check check yourself
How does progressive disclosure keep Agent Skills cheap, and what is loaded into context before a Skill matches?
Show answer
name and description (tiny metadata labels) stay in context at all times; the full SKILL.md body loads only when a task matches. So comparing five Skills with none matched costs only the metadata (~100 tokens) instead of ~2600 tokens if every body were loaded.Why is the description in a Skill's YAML front-matter considered the trigger, and what makes one reliable versus one that never fires?