Skip to content

Course · Pi · Skills & packages

Package the harness and hand it to a teammate

Everything you’ve built for stash (the course’s running example, a small FastAPI feed reader) across the last two modules lives as separate files: stash-tools.ts from your first extension, damage-control.ts, tilldone.ts, plan-injector.ts, and statusline.ts from the rebuilt-defaults module, and now add-feed-source/SKILL.md and triage-fetch.md from this one. Count them - that’s seven files, spread across three directories: five in .pi/extensions/, one in .pi/skills/, one in .pi/prompts/. That’s fine as long as it’s just you working the codebase. It stops being fine the moment a teammate clones stash and asks how to get the same setup: right now the honest answer is “copy these seven files from these three directories,” which is the kind of instruction that gets followed once and then quietly drifts.

A Pi package is the fix: the distribution unit that bundles Extensions, Skills, Prompt Templates, and Themes together and ships them as one installable thing. It ships over infrastructure that already exists - npm or git. There’s no bespoke Pi registry to publish to: a package is just an npm package (or a git repo) that happens to carry a pi key in its package.json. It doesn’t even need to, as it turns out - watch what Pi finds with no manifest at all before you reach for one.

What convention finds, before you write a manifest

Section titled “What convention finds, before you write a manifest”

Predict this before reading on: publish those seven files with no pi key anywhere in package.json, just the files sitting in directories named extensions/, skills/, and prompts/. How many does Pi actually pick up on install?

All seven. This is convention-based auto-discovery, and it runs whenever a package skips the pi key entirely: Pi looks for an extensions/ directory and takes every .ts/.js file inside it - your five extensions, matched. It looks for a skills/ directory and takes every folder inside it that contains a SKILL.md - your one Skill, matched, sitting in its own add-feed-source/ folder exactly as the last lesson built it. It looks for a prompts/ directory and takes every .md file inside it - triage-fetch.md, matched. There’s also a themes/ directory convention, scanned for .json files, and it would pick up stash-dark.json too - if you chose to bundle it. This harness deliberately doesn’t: a theme buys nothing the model needs, so there’s no reason to force your color choices on a teammate who installs the rest. That’s why the count above stayed seven.

Name the directories the way Pi expects, and the bundle needs no manifest at all.

The manifest: a pi key in package.json, when you need precision

Section titled “The manifest: a pi key in package.json, when you need precision”

Convention works because this package happens to be single-purpose - every file in extensions/ belongs to the stash harness, nothing else is mixed in. The moment that stops being true - say extensions/ also holds an unrelated script, or your skills live somewhere convention won’t find - you need to say so explicitly, in the package’s own package.json:

{
"name": "@you/stash-harness",
"keywords": ["pi-package"],
"pi": {
"extensions": ["./extensions"],
"skills": ["./skills"],
"prompts": ["./prompts"],
"themes": ["./themes"]
}
}

The pi key is opt-in - its paths (which support globs and exclusions) tell Pi exactly where to find each resource type inside the package, overriding whatever convention would have guessed. For a package built around one coherent purpose, like this harness, convention already gets the right answer; you’d reach for the explicit pi key mainly to be precise about paths or to exclude something a bare directory scan would wrongly sweep in.

The manifest buys precision; convention buys not needing one. Most single-purpose bundles, including this one, don’t need the manifest at all.

A separate detail, worth knowing but not part of that tradeoff: keywords: ["pi-package"] is the one line that matters for discoverability - it’s the tag that gets a package listed in the gallery and in npm keyword search. You don’t need it for a private team package installed straight from git; it only matters once you’re publishing for strangers to find.

Terminal window
pi install npm:@foo/bar@1.0.0
pi install git:github.com/user/repo@v1
pi install https://github.com/user/repo
pi install ./relative/path/to/package
pi install /absolute/path/to/package

Companion commands round out the lifecycle: pi remove, pi list, and pi update - bare pi update refreshes the pi CLI itself, --all also updates packages and reconciles pinned git refs, and --extensions updates only the installed packages, leaving the CLI alone (the flag’s name is narrower than its scope: packages were the extension-distribution mechanism first, and the name stuck). By default install and remove write to your user settings; -l writes them to .pi/settings.json instead, which means the install line can be committed with the repo - and once a project is trusted, Pi installs any missing packages from it on startup. Commit the -l line and a teammate who trusts stash gets the harness without even running the install themselves.

None of this is stash-specific machinery - it’s the same install surface for anything in the ecosystem, which is exactly why packaging your own stash harness this way means a teammate’s onboarding step becomes one command instead of a conversation.

Once your harness is pushed to a git remote your teammate can reach, the whole thing installs in one line:

Terminal window
pi install git:github.com/your-org/stash-harness@v1

That @v1 isn’t decorative. Drop it - pi install git:github.com/your-org/stash-harness with no ref - and you get whatever commit happens to sit at the tip of the default branch the moment the command runs. That’s also whatever commit is there the next time a teammate installs, or the next time someone runs pi update. Those can be two different commits, and this package’s extensions run with full system access - the damage-control gate alone can read and write anything stash can touch. Pinning to a tag freezes the exact code you reviewed; an unpinned install is a standing bet that whoever controls that repo never ships something worse between now and the next install. Review what you’re pinning to, not just that you remembered to pin it.

What installing doesn’t give you: sync. pi install copies a package’s files onto your machine at that moment - it’s not a live link back to the source. Push a fix to stash-harness tomorrow and every already-installed copy is silently behind, yours included, until someone runs pi update - which isn’t automatic either. Two teammates who installed the same git: URL a week apart can be running two different versions of your own damage-control rules without either of them realizing it. For a harness carrying rules that gate writes to a real stash.db, that’s worth saying out loud in your own onboarding doc: pin the version, and write down that pi update is a step someone has to remember, because Pi won’t remember it for you.

One honest wrinkle in the bundle itself: .pi/damage-control-rules.yaml isn’t an extension, skill, prompt, or theme, so it isn’t one of the four resource types and the package doesn’t carry it. That’s correct by accident - the file encodes stash’s own paths (stash.db, .pi), which belong to the repo, not to a bundle. The extension ships in the package; its rules stay home where the database is.

And because packages support per-resource filtering - the packages array in settings takes an object form ("extensions": [] loads none of them), and pi config toggles resources interactively - a teammate who wants your Skill and template but not your particular damage-control rules can install the package and switch off just the extensions piece.

Here’s how that bundling maps across five tools that ship a similar idea. Pi bundles all four resource types by design - that’s this whole lesson. The others reach the same goal by different roads, and not all of them get there at all. Before you click through: guess which one of the five bundles nothing whatsoever - no plugin format, every piece a loose file a team copies by hand, the exact “seven scattered files” problem this lesson just solved for Pi.

Showing how Claude Code packages plugins.
  • in the pluginSkillLands in the plugin’s skills/ directory.
  • in the pluginSubagentagents/<name>.md inside the plugin.
  • in the pluginHookShips in the bundle and registers on install.
  • in the pluginMCP serverServer config ships in the bundle.
  • in the pluginSlash commandNamespaced on install: /team-stack:pre-merge.
how it ships
/plugin marketplace add our-org/team-plugins
/plugin install team-stack@team-plugins
  • distScopes: user by default, --scope project commits the install to .claude/settings.json, and managed scope lets an org enforce it. The official claude-plugins-official marketplace is preinstalled.
  • noteInstalled plugins are copied into ~/.claude/plugins/cache/ - a plugin can’t reference files outside its own directory.

Cursor’s the outlier: skill, subagent, hook, MCP config, and command all ship loose, because there’s no plugin format to bundle them into yet. If your team is coordinating a shared setup there, you’re doing by hand - copy these files, tell people where they go - exactly what a pi install does in one line here.

Discovery starts at the package gallery - pi.dev/packages, a searchable catalog of everything published with the pi-package keyword: filter by name, description, or author; narrow by resource type (extension, skill, theme, prompt); sort by downloads or recency. Each listing shows a copyable pi install command plus links to the npm package and the source repo, so “found it” and “can read the code” are one click apart. The gallery is a catalog, not a registry - every entry is an npm package underneath, and getting listed means npm publish with the keyword, not signing up for a Pi-specific service. Below the gallery the rawer surfaces are npm’s pi-package keyword search and the Pi community on Discord - then inspect source before installing, always.

A few packages worth knowing about for landscape, not as an endorsement to install any of them blindly: a web-access bundle that adds search/fetch/GitHub-clone tools, a subagent task-delegation package, MCP-related packages (Pi itself gained built-in MCP support in 0.99, referenced back when you built your first extension, so check what a package adds beyond that), and a cross-tool context/MCP plugin that also targets Claude Code and Gemini CLI. The shape of that list is itself informative: on Pi, “add subagent delegation” is just a pi install away, not a built-in feature you’re missing - a fact worth remembering, because the next module builds delegation from first principles and you’ll want to compare.

The docs put this plainly, and it applies to a package exactly as much as it applied to a single Skill a lesson ago: a Pi package runs with full system access, and its extensions execute arbitrary code. Installing someone’s pi-package is installing their code onto your machine with the same reach Pi itself has - no sandboxing layer specific to packages, and no version pin means no guarantee the code you reviewed is the code that runs next time. Review before you install, especially anything pulled straight from a git URL rather than a name-checked npm publish. The convenience of pi install git:github.com/whoever/whatever is real; so is the fact that it will run whatever’s in that repo, today and every time it’s reinstalled after.

Seven files, three directories, one command - the ratio promised at the start of this module holds. The four no-code layers line up cleanly by when they spend and who fires them: a Skill pays its frontmatter at startup and its body only on a matching request (you wrote the description; the model decides when it matches); a template pays nothing until you type it, with no model discretion at all; a theme never touches the window - it is rendering cost and human comfort, nothing the loop sees; and a package inherits all three of those price tags for whatever it bundles, costing nothing at install time beyond them - with one exception this page’s own bundle makes important: extensions. An installed package’s extension code loads at session start (its tool schemas join the window then, called or not, and the code runs with full reach - the security section’s whole point), so a bundle carrying extensions spends from the first session, not only when a job calls. Skills, templates, and themes hold to the nothing-until-fired rule, and every firing decision traces back to a name - yours, or a rule you wrote.

You’ve now got a stash harness that isn’t just yours anymore - it’s one pi install away for anyone on the project. But notice what didn’t change across any of the four rows: everything still loads into one window. Yours. A Skill’s body, a template’s expansion, a whole package’s bundle - wherever they come from, they all land in the same context you’ve been managing since the core chapter first showed you the window. That’s fine for every job you’ve handed it so far. It stops being fine the day the job itself stops fitting in one window - and that day arrives in the next module, delivered by email. Next: build subagents from first principles.