Write the content-source-onboarding procedure as a SKILL.md
Every time a teammate wants stash to save from a source it doesn’t handle yet, you’ve been doing the same five things by hand: look at a sample page from the new source, work out what its markup does to the readability extractor, write a small adapter, register it with the fetch worker, run it against a real URL and check the article text came out clean instead of full of nav cruft. You’ve re-explained that procedure to Pi twice now, typing out the same steps in slightly different words each time. That’s the tell. When you catch yourself re-narrating a multi-step routine instead of just naming it, it’s time to write it down once - and this time the right format for “write it down” isn’t TypeScript.
Skill and extension are different layers
Section titled “Skill and extension are different layers”An extension - what you built in the last two modules - is a TypeScript module that registers a tool, a command, or a hook through the ExtensionAPI. It’s code: it can enforce, block, compute, reach out to the fetch worker directly. A Skill is the layer below that. It’s markdown with a bit of YAML on top, and it can’t do anything an extension couldn’t already do more directly - a Skill has no execution semantics of its own beyond “here are the instructions, go follow them” (though its body can absolutely tell the agent to run a script that ships alongside it). What a Skill buys you instead is that you can write one without touching TypeScript, and that it stays nearly free in context until the moment it’s actually relevant.
That second part is the mechanism worth understanding before you write one: progressive disclosure. At startup, Pi scans every Skill it can find and pulls only two fields - name and description - into the system prompt. The rest of the file, and anything else in the Skill’s directory, stays on disk. Only when your request matches a Skill’s description does Pi reach for its read tool and pull in the full body. You can keep a dozen Skills around for a dozen stash chores and pay for the one that fires, not the other eleven.
The file structure
Section titled “The file structure”A Skill is a directory with one required file inside it:
onboard-content-source/├── SKILL.md (required)├── scripts/├── references/└── assets/scripts/, references/, and assets/ are optional siblings - a helper script the body tells Pi to run, a longer reference doc, a template file to copy. None of it loads until the body of SKILL.md actually points at it. For the onboarding procedure, a single SKILL.md covers it; you reach for the subdirectories only once a Skill’s instructions genuinely outgrow one file.
Pi looks for Skill directories in several places, checked in order:
~/.pi/agent/skills/and~/.agents/skills/- global, yours across every project.agents/skills/- discovered by walking up from your current directory to the repo root.pi/skills/and.agents/skills/in the project itself- a
skills/directory (orpi.skillskey) inside an installed package - the subject of the packages lesson - a path passed with
--skill <path> - a
skillsarray insettings.json
Since this procedure is stash-specific - it references the fetch worker and the readability extractor by name - it belongs in the repo, in .pi/skills/ or .agents/skills/, committed so anyone who clones stash gets it too.
Frontmatter: the label Pi reads before it opens the box
Section titled “Frontmatter: the label Pi reads before it opens the box”Here’s the whole Skill:
---name: onboard-content-sourcedescription: >- Onboard a new content source into stash's fetch worker when a URL from an unfamiliar site or platform isn't extracting cleanly - nav/ads leaking into the article text, or the extractor returning empty content. Use when the user wants to add support for a new site, wire up a new source, or fix a source that's extracting badly.---
Follow these steps when a source needs onboarding or repair. Ask for areal sample URL from the source if one isn't already in the request -do not guess at a site's markup.
1. Fetch the sample URL and inspect the raw HTML structure around the article body: what wraps the real content, what wraps nav/ads/related links.2. Check whether the current extractor already gets close. If it's leaking specific boilerplate (a cookie banner, a related-posts block), that's usually a targeted selector fix, not a new adapter.3. If the site's shape is different enough to need its own path, write a small source adapter under the extractors directory, following the structure of the existing ones.4. Run the adapter against the sample URL. Confirm the stored article text has no leftover nav/ads and no missing paragraphs.5. Confirm the new entry is searchable: check it shows up in a full-text search for a distinctive phrase from the article body.6. Report back a short diff of raw HTML vs. extracted text so the user can eyeball the extraction before it goes live for everyone.Only two fields are required, and they’re the only two doing any work at Skill-selection time:
name- 1 to 64 characters, lowercase letters, numbers, and hyphens only. Unlike the plain Agent Skills standard, Pi doesn’t require this to match the directory name - a deliberate deviation, so a shared skills repo can be reused across several tools without renaming folders to suit each one.description- up to 1024 characters, and it’s the entire trigger surface. Pi’s system prompt carries this string for every Skill it knows about; when your request semantically matches it, the Skill fires. Notice the description above names the situation (“a URL from an unfamiliar site isn’t extracting cleanly”) rather than just an action - that’s what lets it match “hey can we add support for this newsletter site” without you ever typing the Skill’s name.
A handful of optional fields round out the spec: license, compatibility (environment requirements), metadata (arbitrary key/value), allowed-tools (a pre-approved tool list for this Skill), and disable-model-invocation - set that last one and the Skill drops out of the system prompt entirely, invocable only by hand. None of them change whether the Skill can fire; they change how much you trust it to fire on its own.
Invoking it
Section titled “Invoking it”Once it’s saved, you can call it directly:
> /skill:onboard-content-source> /skill:onboard-content-source https://example-newsletter.com/p/some-postor just describe the problem and let the description do the matching:
> stash is choking on this Substack link, the article text has the subscribe-box and comments mixed into itEither way Pi’s read tool is what actually loads the body - you can watch that happen in whatever context view you built two modules back. The /skill: slash-command surface itself is a toggle: enableSkillCommands in settings.json controls whether the explicit /skill:name form exists at all, separate from whether the model can invoke a Skill on its own recognition.
Here’s the shape of a SKILL.md, and where it sits relative to how other agents handle the same idea - click through the parts:
The security line, same as any code you didn’t write
Section titled “The security line, same as any code you didn’t write”The docs are blunt about this, and it’s worth taking at face value: a Skill’s body is instructions the model will follow, and it can tell Pi to run arbitrary scripts sitting right next to it in scripts/. Installing someone else’s Skill - which the next-but-one lesson is entirely about - deserves the same read-before-you-trust caution you’d give an extension. A Skill being “just markdown” doesn’t mean it’s inert.
You’ve now written a stash procedure down once, in a format Pi loads only when it’s needed. Two lighter no-code layers are worth the same five minutes each - a shortcut for the prompt you type often, and a theme for the terminal you’re staring at all week. Next: templates and themes.