npx skills add ...
npx skills add posthog/ai-plugin --skill working-with-skills
Best practices for agents managing PostHog skills via the MCP `skill-*` tools — how to discover, read, create, update, and refactor skills efficiently, especially large skills with many bundled files. Use whenever you are about to call any `skill-*` tool, asked to author or edit a shared skill, or troubleshoot why a skill write was rejected. Pairs with `skills-store` (which covers the raw tool surface) by adding the decision-tree, efficiency, and pitfall guidance.
npx skills add posthog/ai-plugin --skill working-with-skills
This skill teaches agents how to use the skill-* MCP tools well — minimum
context, minimum round-trips, minimum mistakes. If you are not yet familiar with
the tool surface itself, read the skills-store skill first for the catalog.
This document is about how to choose between the tools and how to scale the
workflow when skills get big.
edits
or file_edits is cheaper, safer, and clearer in version history than a
full body or full bundle replacement.version from skill-get (or from the response of the previous write)
before calling any write tool, and pass it as base_version.name kebab-case, descriptions trigger-rich, body short, bulky
material in bundled files.If you find yourself reaching for update(body=...) plus a sprawling files=[...]
to change one paragraph and one script, stop — that's two narrower calls
(update(edits=[...]) plus update(file_edits=[...])) or even a single
update carrying both edits and file_edits.
skill-list is the right tool to "find a skill" — it returns names and
descriptions only. Reading the descriptions is the entire point: pick the right
skill before pulling any body. If search doesn't narrow it enough, list
without it and scan, but do not start fetching candidate bodies blindly.
skill-get should be called once per skill per task, not per question.
Cache the body in your working memory; fetch again only if you suspect the
skill changed under you (e.g. a 409 on write — see "Concurrency" below).
Big skills (long body, many bundled files) are the case where lazy loading matters most.
skill-get(skill_name=...) — read body + files[] manifest.skill-file-get(file_path=...). Skip everything else.scripts/X.When in doubt, fewer files. You can always fetch one more on the next turn.
Use a single skill-create call with body and initial files — the
skill lands at version: 1 complete. Do not create the skill empty and then
make N follow-up skill-file-create calls; that's N extra versions and N
extra round-trips for no benefit.
description is the discovery surface. It is the only thing
skill-list returns. Make it trigger-rich (what the user might say) and
scope-honest (what the skill does and does not do).name — kebab-case, max 64 chars, no leading/trailing/consecutive
hyphens. The spec validator rejects anything else.references/, assets/, or scripts/. The body
should route to those files, not inline them.scripts/ for executable code, references/
for prose docs and examples, assets/ for templates / data. Agents can rely
on this for orientation when they only have the manifest.allowed_tools lists the MCP / built-in tools the skill expects to be
callable. Be honest — under-declaring causes silent failures, over-declaring
is a security smell.## Related skills footer when adjacent skills exist: a short
bullet list of `skill-name` entries, each with a one-line handoff reason
("when to jump there"), so one skill invocation seeds discovery of the next.
Refer to skills by name only (no paths — related skills often live in other
products), and only list genuine next steps, not everything in the product.The single most common mistake is using update(body=..., files=[...]) for a
small change. That works, but it round-trips the entire skill, makes the diff
unreadable in version history, and risks dropping files if files was
incomplete. Use the smallest primitive instead.
versionNote the returned version — pass it as base_version on every write. After a
successful write, the response contains the new version; chain further writes
with that.
Full replacement when you are restructuring the body:
Incremental edits when you are tweaking a few lines (preferred for small changes — easier to review, lower error surface):
Each edits[].old must match exactly once in the current body, and body and
edits are mutually exclusive in one call.
file_edits patches one or more existing files in place — non-targeted files
carry forward unchanged. This is the right primitive when you are tweaking
script logic or fixing a typo in a reference doc:
file_edits cannot add, remove, or rename files — only patch
existing ones. For structural changes, use the per-file tools.
You can combine edits (body) and file_edits (existing files) in one
skill-update call to publish a single coherent version when a change
spans both:
The same concept — a bundled file's path — is named differently depending on where it travels in the request, and this trips up agents working from memory. There is one rule:
file_path — when the path is part of the URL (skill-file-get,
skill-file-delete). These read/delete one file addressed by its path.path — when the path is a body field: skill-file-create, the
files=[{path, content, content_type}] array, and file_edits=[{path, edits}].old_path / new_path — body fields on skill-file-rename.Mnemonic: path is the field name on a file object (it sits next to
content), so everything that carries a file object uses path; the two
tools that address a file by URL use file_path. When unsure, check the
tool's input schema rather than guessing — passing path to file-get yields a
/files/undefined/ 404.
Each is its own call, each publishes a new version:
skill-file-rename is a true move — it carries the existing content
forward without resending it. Always prefer it over delete + create when the
content is unchanged.
update(files=[...]) (rare)Passing files to skill-update replaces the entire bundle —
anything not in the array is dropped. This is the right tool only when you are
intentionally wiping and reseeding the bundle (e.g. importing a fresh local
SKILL.md tree). For almost every other case, prefer file_edits plus per-file
CRUD.
Skills with many files (10+) require extra discipline:
skill-get's files[] is your map.
Match each task step to one file and fetch only that one.rename → rename → rename, each chained
via the previous response's version. That gives you three small reviewable
versions instead of one giant update(files=[...]) blob.skill-update with file_edits
targeting five files is fine. A single update(files=[...]) carrying ten
full file bodies is almost always a sign you should have used file_edits.base_versionEvery write tool accepts base_version. Always pass it.
base_version to the current latest version. If they
match, the write succeeds and the new version is base_version + 1.skill-get, reconcile your changes against the new body, and
retry with the fresh version.version. Chain
further edits with that — do not re-get between back-to-back writes you
control.Skipping base_version does not speed things up — it just turns a clean
"someone else won the race" error into a silent overwrite of their work.
skill-list with no search and then fetching every body —
defeats progressive disclosure. Read the descriptions first.skill-get — same mistake on
the inner level. Fetch on demand from the body's directives.update(body=..., files=[...]) for a one-line fix — round-trips
the entire skill, makes diffs unreadable, and risks dropping files. Use
edits / file_edits.update(files=[...]) when you meant to add one file — drops every
file you didn't include. Use skill-file-create instead.base_version after chained writes — read the version from the
previous write's response, not from your initial get.base_version off — accepts a silent overwrite. Always include
it once you've done a get.description — the skill becomes effectively undiscoverable
via skill-list search. Treat the description as the trigger contract.references/ and scripts/ rather than letting it grow.body and edits in one update call — they're mutually exclusive.
Pick one.path vs file_path — file-get and file-delete take file_path
(it's in the URL); create, rename (old_path/new_path), files, and
file_edits take path (it's a body field). See "File-path parameter
naming" above.skill-archive hides every active version of a skill by name. It is
not version-scoped and cannot be undone — the skill drops out of
skill-list and skill-get for the whole team.
Before archiving, skill-get the skill if you need to inspect or copy it
first. Archiving is the right tool for retiring a skill entirely; to remove a
single bundled file use skill-file-delete, and to roll back content
publish a new version rather than archiving.
When migrating a local skill folder (e.g. my-skill/SKILL.md plus
scripts/, references/, assets/):
SKILL.md. Its frontmatter maps to name, description,
license, compatibility, allowed_tools, metadata. The body after the
frontmatter becomes body.{ path, content, content_type }.posthog:skill-create once with everything — the skill lands at
version: 1 complete. Do not split this into a create + N file-create
calls.After the create, the skill is live for everyone via skill-get.
Not every persistent prompt belongs in the skills store:
A good skill is reusable, discoverable by description, and worth the cost of keeping it correct over time.