Author a skill
Take a topic and end at a linted SKILL.md draft plus a runnable
fresh-session test plan. The input is a subject, not a specification —
deciding the name, the boundaries, and what the skill deliberately will
not cover is the work, not a precondition for it.
This skill is house-style. It encodes how this repo builds skills; the generic mechanics of the Agent Skills format are somebody else's job (see step 1).
Repo-relative paths below are relative to the agent-config repo
(C:\Repos\Personal\agent-config), not the session's cwd.
~/.claude/skills is a real directory holding one junction per
skill into that repo, so this skill can fire from a session in any
repo — resolve paths against agent-config regardless of where it fired.
An edit is immediate; a new skill needs the linker once. The
asymmetry follows from those per-skill junctions. Editing an
already-junctioned skill is live the moment it hits disk, for every
session on this machine, committed or not. But a directory that did not
exist at the last scripts/link-claude.ps1 run has no junction, so
a newly authored skill is invisible everywhere — absent from the
listing, and /<name> answers Unknown command. Measured 2026-09-02:
land was missing from a listing of 8 junctions immediately after its
SKILL.md was written, and appeared only once the linker ran. On this
machine the form is ./scripts/link-claude.ps1 -SkillGroups workflow,
never bare — see root CLAUDE.md.
A platform skill is the exception that proves the rule: fabric and
powerbi are pruned from user scope here on purpose, so a new one stays
unlinked by design and enters no session's payload. Nothing is broken
when that happens, and no linker run changes it.
A project-scope skill needs the linker not at all, which is the
third case and the easiest to get wrong in the other direction. Nothing
junctions .claude/skills/ — Claude Code reads it in place — so a new
skill there is live on save for sessions in this repo, with no deploy
step to forget. Do not run the linker "to pick it up"; it selects out of
skills/ and will not see it. The trade is that it is live only here,
which is the whole reason it is there.
Once linked, a half-drafted skill is a live half-drafted skill; finish the frontmatter before walking away.
1. Check this is the right skill
Two routes out, both cheap to check first.
Guidance that already exists → /learn. The dividing line is whether
the destination exists. /learn folds a session learning into a skill,
rule, or CLAUDE.md that should already have covered it. This skill
creates guidance that has no home yet. A learning that arrives as "the
fabric-cicd skill was wrong about X" is /learn work even if the fix
is large.
Generic skill-authoring help → skill-creator or
plugin-dev:skill-development. Both are loaded and both are good.
They are not wrong, only generic: they know the Agent Skills format,
progressive disclosure, and description tuning in the abstract. They do
not know this repo's naming rules, its lint command, its references/
split, its junction deployment, or the discipline of writing a handoff
brief first. That gap is the entire reason this skill exists alongside
them. Say so plainly when the overlap comes up, so the duplication reads
as deliberate rather than as something nobody noticed.
If the user wants a subagent rather than a skill, this is the wrong
skill: the artifact lives in claude/agents/ and uses
templates/subagent-handoff.md. Stop and say so.
2. Check for existing coverage
Before naming anything, find out whether the topic is already covered.
grep -li "<topic term>" skills/*/*/SKILL.md .claude/skills/*/SKILL.md
grep -n -i "<topic term>" skills/fabric/fabric-gotchas/SKILL.md
Check fabric-gotchas explicitly. /learn treats it as the default
home for cross-product troubleshooting content, so a topic can be
half-covered there without any dedicated skill existing.
An incidental grep hit is not coverage. The worked failure is a
Mirroring search matching "Spark History Server mirror" in
fabric-spark-monitoring — same substring, unrelated subject. Read the
surrounding heading before counting a hit. Conversely, a topic can be
genuinely covered under vocabulary you did not grep for; skim the
skills/README.md section list for the relevant domain as a second
pass.
Three outcomes:
- Covered correctly — stop. Say where, and that nothing is needed.
- Covered partially, in a skill that owns the domain — stop and propose extending that skill instead. A second skill splitting one domain makes both harder to trigger, because the model is choosing between two descriptions that both half-match.
- Not covered — continue.
The stop cases are real stops. Do not proceed to drilling because the topic is interesting.
3. Decide the name and namespace
Put the proposal to the user before drilling. Naming is cheap to change now and expensive later — the name is in the directory, the slash command, and every cross-reference.
Behavioral, cross-domain skills take the verb you invoke —
commit,learn,code-review,drift-audit. Read the name as the user typing it.Platform skills take a namespace prefix —
fabric-,pbir-, orpbid-.powerbi-*is reserved. Those are vendored frommicrosoft/skills-for-fabricand keep upstream naming so re-sync diffs stay clean. Never take that prefix for a local skill.Pick the TREE before the group, and pick it by asking what the skill acts on — not from the namespace, which cannot answer this. Does it act on this repo, or on the user's own work?
- Acts on
agent-configitself — its groups, its linter, its handoff queue, its audit ledger:.claude/skills/<name>/SKILL.md, project scope. No group directory. It deploys nowhere and no script reaches it, which is the point: outside this working tree it has nothing to act on, so shipping it to user scope would put it in the startup listing of every client-repo session for no possible benefit. - Acts on the user's work — code, Fabric items, reports, any repo:
skills/<group>/<name>/SKILL.md, deployable payload.
Getting this wrong is silent and costs listing budget everywhere. The seven skills at project scope today (
author-skill,test-skill,learn,drift-*,land) were all at user scope until 2026-09-09 for exactly this reason.- Acts on
Within
skills/, the namespace picks the group directory, and the group directory is load-bearing.skills/fabric/forfabric-*,skills/powerbi/forpbir-,pbid-,pbip-and the vendoredpowerbi-*,skills/workflow/for the behavioral ones — which now means the repo-general verbs only,code-reviewandcommit.Depth is pinned per tree and a misplacement fails twice silently. The pre-commit hook matches
^(skills/[^/]+|\.claude/skills)/[^/]+/SKILL\.md$— two directories underskills/, one under.claude/skills/. A skill placed flat atskills/<name>/SKILL.md, or nested a level deeper in either tree, is invisible to the linter and to Claude Code, which discovers skills exactly one level under a skills root.Name the job, not the target, where they differ.
drift-auditis named that way because it audits rules,CLAUDE.md, and the MCP templates too —skill-auditwould have named a quarter of its scope and collided with a plausible future skill.The linter enforces the mechanics: lowercase letters, digits and hyphens only, ≤ 64 chars, and no
anthropicorclaudeanywhere in the name.
Check the name against the loaded plugin skills, not just this repo.
A local skill competing with skill-creator or a plugin-dev:* skill
for the same trigger surface is a real collision even though the
directories never touch.
Also settle the scope question at this point: is this one skill or
two? A topic that splits cleanly into a reference half and a workflow
half is often two skills (pbir-cli and pbir-report-workflow are the
in-repo example). Decide with the user now; discovering it during
drafting means rewriting the brief.
4. Drill the sources
Do not write from training data. The point of this step is that every claim in the finished skill traces to something read during this run.
- Microsoft Learn —
microsoft_docs_searchto find the pages, thenmicrosoft_docs_fetchfor the ones that matter. Search returns 500-token excerpts, which are enough to locate a page and never enough to encode a constraint from. - Exact repo bytes, changelogs, release notes —
github-mcp. - Anything else —
WebFetch.
MCP servers are deliberately absent from this skill's allowed-tools
so it does not fail closed on a machine without them. Prefer them when
present; fall back without ceremony when not.
Record what was drilled and what was not. The undrilled set is what bounds the draft, and it is the part that gets lost if it is not written down at the time. A brief that says "the REST surface was not drilled; nothing in this skill describes it" is what stops the next reader assuming the omission was an oversight. The template's Sources drilled section is where both halves go — not Notes, and not a sentence buried in Scope.
Stop drilling when new pages stop changing the outline, not when the source list is exhausted.
5. Write the handoff brief
The target is docs/handoffs/execute/<name>.md, built from
docs/handoffs/templates/skill-handoff.md.
Check whether that path is already occupied before writing a byte.
A /drift-update escalation leaves its scoping input at exactly this
name, so the file that authorizes the work and the file this step
produces collide by default. Such a file is normally untracked, which
makes an overwrite unrecoverable — git has nothing to restore.
If something is there, read it in full and treat it as source rather than as an obstacle. Carry across everything that outlives it — the authorization, the audit evidence, the undrilled set, and anything citing a gitignored path that will not survive on its own — then confirm with the user that the file may be replaced. Never overwrite a brief path unread.
Every heading in the template survives into the filled brief.
Sections that do not apply get N/A — <brief reason> under the heading,
never deletion.
Strip the per-section > Guidance: notes. They are instructions for
filling the template, not content, and a brief that keeps them reads as
half-finished. Exactly two blocks are reproduced verbatim: the guidance
note directly under the title, and Claude Code's post-draft checklist.
docs/handoffs/examples/author-skill.example.md is the reference
— two guidance blocks in the finished brief, not one per heading. The
other two examples predate this convention and strip both; follow the
author-skill one.
Fill Last verified with today's date.
The brief is not ceremony and not a handoff to another surface. It is the record of what was decided and what was deliberately left out, and the excluded set is the half that cannot be reconstructed from the finished skill. Write it before drafting even though the same session does both — the ordering is what makes the scope decisions explicit instead of emergent.
If an existing brief in docs/handoffs/examples/ matches the new
skill's shape, lift it as design source and record that lineage in
Changes from source proposal using the wording that
examples/README.md specifies, rather than re-narrating the design.
Show the brief to the user before drafting from it.
6. Draft the SKILL.md
Write the SKILL.md from the brief, at the path the tree decision in
step 3 picked — skills/<group>/<name>/SKILL.md for payload,
.claude/skills/<name>/SKILL.md for a skill that maintains this repo.
The description is the entire model-invoked trigger mechanism.
Write it to fire on the queries the skill should answer — the user's
vocabulary, error strings, tool and command names — not to summarize the
body. A description that reads as an accurate abstract and never
triggers has failed at its only job. Where the skill neighbours another,
spend a clause on the disambiguation.
A disambiguation pointer must be reachable from where it is read. A
paths:-scoped skill is in the listing only once one of its own globs
has matched, so "use <other-skill>" is dead whenever the two share no
trigger file. The failure is worse than a no-op: the model calls the
Skill tool, gets Unknown skill, and reports that skill as not
installed — a false diagnosis layered on a dead pointer, in a session
where it was deployed and merely not activated. So point at a co-active
neighbour by name, and point off the shared surface by file —
opening the file is what activates the skill there, which makes the file
the only pointer that works cold. Measured 2026-09-04: pbir-filters
said "for filter-pane styling use pbir-themes", and their globs are
disjoint.
Long detail goes to the skill's own references/, not the body. Root CLAUDE.md is explicit about this. Command flag tables,
per-item-type matrices, and long worked examples belong in a reference
file the body points at. The linter caps the body at 500 lines, but that
is a backstop, not a target.
If you are relocating content out of an existing body rather than
writing new, verify nothing was lost before committing. Moving prose
between files is where silent content loss happens — it happened twice
during the 2026-08-31 body-slimming pass, once from a substring heading
match that dropped a whole section. Match headings exactly, with an
assertion, never by substring, and diff the result against HEAD:
n = lambda s: re.sub(r'\s+', ' ', s).strip()
u = lambda s: set(re.findall(r'https?://[^\s\)\|]+', s))
print("URLs lost:", u(orig) - u(new_skill_plus_all_references))
missing = [l for l in orig.split('\n') if len(n(l)) > 45 and n(l) not in n(new)]
Every surviving entry in missing must be explainable as an intentional
rewrite. An empty missing and an empty URL set is the pass condition.
Match the house voice: numbered steps, bold lead-ins for the rule being stated, an explicit constraints section at the end, and reasons attached to rules that would otherwise look arbitrary.
Nothing goes in that drilling did not establish. A plausible claim
with no source behind it is the failure mode this whole procedure exists
to prevent. If something is believed but unverified, mark it as such
inline with the date and version, the way /learn does, so a later
/drift-audit can confirm or remove it.
7. Post-draft checks
Run all four. Each catches something the others do not.
uv run --with pyyaml scripts/lint-frontmatter.py <the SKILL.md you wrote>
Re-count the description. The linter gates description at 1,024
and when_to_use at 512 — the two halves of the 1,536 listing
truncation point — but it reports overflow only after the fact and
never warns on a near miss:
uv run --with pyyaml python -c "import sys,yaml; print(len(yaml.safe_load(open(sys.argv[1],encoding='utf-8').read().split('---')[1])['description']))" <the SKILL.md you wrote>
1,024 is the Agent Skills spec cap, and description is one of the six
fields the claude.ai upload path accepts — so that is the field that has
to stay portable. Re-count after any wording change, not once at the
end. If the skill sets when_to_use, count it too against its own 512:
it is a Claude Code extension the spec does not carry, so spending the
remainder there costs no portability the skill still had.
cat the whole file after any edit. YAML frontmatter is a single
malformed line away from the skill silently not loading, and the Edit
tool on Windows is where that line comes from.
pre-commit run --all-files
8. Register it in skills/README.md
Add the entry to the section the namespace implies — Behavioral, Microsoft Fabric platform, or Power BI Desktop / Reports. Update the count in the section heading where one is present.
Match the house style there: what the skill covers, and — where the name
is not self-evident — why it is named that. The drift-audit and
drift-handoff entries are the model for the second half. An entry that
only restates the description earns nothing the description does not
already do.
This is the one place this skill edits a file it did not create. Keep the diff to the single added entry and the count.
9. Report, sweep, hand off
Report:
- What was drilled and what was not. The undrilled set, verbatim from the brief.
- The scope decisions — what the skill deliberately does not cover, and why.
- Check results — lint, description count,
pre-commit. - That the skill is not behaviourally tested. Say it plainly —
but for the right reason. It is not that the file cannot reload:
skills hot-reload in-session, fixed upstream in 2.1.216 and verified
here 2026-08-31 and again 2026-09-02 on an in-place
descriptionedit. It is that the session which wrote the skill is the worst place to test it — accumulated context can mask a co-load failure, and you already know the answers you are checking for. A cold run against a--safe-modebaseline is the test. Name the specific queries it should fire on, so the fresh-session test is runnable rather than aspirational — those queries are what/test-skillreads back out of the brief, so a vague one here becomes a vague test there.
Then sweep docs/handoffs/execute/ for briefs whose work has
landed. A queued brief is deleted once its change lands, or promoted
into examples/ in the one case where something cites it. That
convention has already failed once unattended, which is why the sweep is
a step here instead of a habit.
- The brief written at step 5 stays queued. Nothing is validated until the fresh-session test has run.
- For any other brief whose skill or subagent now exists, propose deletion or promotion and name which. Propose rather than act: you can see that an artifact exists, but not whether the user considers the work landed. Deletion itself is safe — briefs are committed when written, so the deleting commit and its content stay recoverable.
Hand off to /test-skill, then /commit. Do not commit here, and
do not test here either. /test-skill reads the brief from disk, so it
can run now or in a fresh session next week — the two do not have to be
back to back, and a cold run is the better one.
10. Constraints
- No unverified claims. If step 4 did not establish it, it does not go in the skill. Marked-unverified is the only exception, and it carries a date.
- The stop cases in steps 1 and 2 are stops. Route to
/learn, propose an extension, or hand off to a generic authoring skill — do not author anyway. - Name before drilling. Naming and the one-skill-or-two call go to the user first; both are cheap now and expensive after a draft exists.
- Brief before draft, even in one session.
- No test fixtures.
tests/is a separate, deliberate exercise — and it is/test-skill's, which writes the fixtures, updatesexpected_activations.mdand runs both the static and the real-path activation test. - No commit, no push.
- Do not edit other skills. Only the new skill's own directory, the
new brief, and its single catalogue entry --
skills/README.mdfor a payload skill,.claude/skills/README.mdfor a project-scope one.Editis available for those two existing files and nothing else — adjacent cleanups are/learnand/simplifyterritory, and an unbriefed edit made here has no evidence behind it. - Deletion is the user's call. Step 9 proposes; it does not sweep files away.