Skills That Carry Files
SKILL.md can reference scripts, references and assets by relative path; here is the layout, the path convention, and how to distribute the whole directory as a package.
The review guard’s checklist is forty items. Three screens of Markdown in SKILL.md would load all forty every time the skill fires, including for the small reviews where only three apply.
skills.md offers a better arrangement: skills can bundle scripts, references, and assets alongside their instructions. The body stays short and names the files; the files load only when the instructions say to read them.
The layout
review-guard/
├── SKILL.md
├── scripts/
│ ├── run-checks.sh
│ └── collect-diff.sh
├── references/
│ ├── checklist.md
│ └── severity-rubric.md
└── assets/
└── finding-template.md
The four directory names are not enforced. skills.md shows exactly this shape in its own example — scripts/, references/, assets/ — and the convention is what makes a skill legible to the next person. Nothing stops you adding templates/ or data/; nothing encourages you either.
Reference files by relative path
SKILL.md becomes:
---
name: review-guard
description: Reviews changed code against this repository's conventions and runs its own checks. Use when reviewing a diff, a pull request, or uncommitted changes.
---
# Review guard
1. Run `scripts/run-checks.sh` and read its output before reviewing anything.
2. Read `references/severity-rubric.md` and classify each finding.
3. Read `references/checklist.md` and check each item that applies to the changed files.
4. Write each finding using the shape in `assets/finding-template.md`.
Report findings ordered by severity. Report nothing when nothing is wrong.
skills.md states the rule plainly: use relative paths from the skill directory when referring to bundled files, because Pi tells the model where the skill lives so it can resolve those paths.
That last clause is the part to rely on. You write references/checklist.md, not an absolute path and not a path relative to the working directory. A skill directory that moves between projects keeps working.
Three ways to use a bundled file
| Kind | Use it for | How it is loaded |
|---|---|---|
references/ |
Detail consulted after the decision to work is made | The model reads it with the read tool |
scripts/ |
Deterministic work that should not be improvised | The model runs it through bash |
assets/ |
Material to copy or fill in | The model reads or writes it with tools |
The distinction that matters is when. A reference file is read once the skill has decided the task is relevant; a script runs when the instructions say to run it; an asset is used at the point of output. Bundle everything into SKILL.md and all of it loads on every fire.
Wrong: "Bundling files into the skill makes them available to the model."
Correct: "A bundled file is a file on disk. It reaches the context when the body tells the model to read it, using the model's own tools."
Scripts earn their place by being boring
A script inside a skill should do something a model would do worse or less reliably by hand. Running the repository’s own checks qualifies:
#!/usr/bin/env bash
# scripts/run-checks.sh - run this repository's own verification.
set -euo pipefail
cd "$(dirname "$0")/../.."
echo "== typecheck =="
pnpm typecheck
echo "== unit tests =="
pnpm test:unit
Note cd "$(dirname "$0")/../.." — the script resolves the repository root from its own location, so it does not depend on the working directory Pi happens to be in.
skills.md says to run scripts relative to this skill directory, and to keep environment setup inside the skill. That is a meaningful instruction: the skill is responsible for its own prerequisites rather than assuming the host has them.
The size rule of thumb
This is my recommendation, not a documented limit. skills.md gives no token budget for a SKILL.md. The reasoning:
- The body loads every time the skill fires. Keep it to the steps, not the reference material.
- If a rule is needed before deciding what to do, it belongs in the body. If it is needed after, it belongs in
references/. - If a file is needed on every single run, it is not really a supporting file; inline it and accept the cost.
A body of fifteen lines that reads three files usually beats a body of three hundred lines that reads none.
Keep bundled scripts reviewable
Chapter 8 said project skills can instruct the model to run scripts, and that unfamiliar skills and their supporting files should be reviewed before granting project trust. That review is only possible if the script is a script — readable text in the repository — rather than a compiled binary or something fetched at run time.
Two habits that help: keep every bundled script versioned with the skill, and make it fail loudly rather than silently continuing:
set -euo pipefail
Without those two lines a check script can exit zero while skipping everything, and the model will report a clean review.
Ship it as a package
skills.md says to use a Pi package to distribute one or more skills through npm or git, and to keep environment setup inside the skill and declare any required runtime dependencies in the package.
packages.md describes what that takes. A package is an ordinary directory or npm package, and without a pi manifest Pi discovers skill directories from conventional locations:
my-pi-package/
├── package.json
├── extensions/
├── skills/
├── prompts/
└── themes/
review-guard/ drops straight into skills/.
With an explicit manifest, resources can live elsewhere or be filtered:
{
"name": "team-review-tools",
"keywords": ["pi-package"],
"pi": {
"skills": ["./skills/review-guard"],
"prompts": ["./resources/prompts/*.md"]
}
}
Paths are relative to the package root. Arrays accept glob patterns and exclusions. If a resource root is dot-prefixed or symlinked, list it directly, because traversal through a glob would not discover it.
Declare dependencies correctly
packages.md is emphatic here, and the reason is technical rather than stylistic. Pi supplies @earendil-works/pi-ai, @earendil-works/pi-agent-core, @earendil-works/pi-coding-agent, @earendil-works/pi-tui, and typebox to extensions and skills. Declare those in peerDependencies with a "*" range and do not bundle them. Do not list them in dependencies: a physical copy can bypass Pi’s extension module mapping in compiled ESM and create duplicate classes, registries, and initialization work, and Pi reports an extension warning when it detects that configuration.
For a skill with a bundled shell script, there are no Node dependencies at all. That is a feature of skills over extensions.
Narrow what loads from a package
The object form in settings filters a package’s resources:
{
"packages": [
{
"source": "npm:@example/team-review-tools",
"skills": [],
"prompts": ["prompts/review.md"],
"extensions": ["extensions/*.ts", "!extensions/legacy.ts"]
}
]
}
Omit the property to load everything the package allows; use [] to load none of that type. Filters narrow the package manifest — they do not expose resources the package did not declare.
Install it
pi install npm:@example/team-review-tools@1.0.0
pi install git:github.com/example/pi-tools@v1
pi install ./local-package
pi -e npm:@example/pi-tools # try it for one invocation
pi list
Versioned npm specifications are pinned, as are git tags and commits; package updates reconcile the checkout but do not move a configured ref. Relative local paths resolve from the settings file that contains them.
Personal installs are written to ~/.pi/agent/settings.json. Add --local or -l to write the declaration to .pi/settings.json, which Pi reads only after project trust is granted.
Identity and double-loading
packages.md explains how Pi identifies a package so the same one cannot load twice: npm packages by package name, git packages by repository URL without the ref, and local packages by resolved absolute path. The same package can appear in personal and project settings; a project entry normally replaces the personal one, and with autoload: false it acts as a filtering delta instead.
Installed packages load with separate module roots. Do not rely on two packages sharing one dependency instance.
The running example, complete at skill scope
review-guard/ is now complete at skill scope: a short body that routes on its description, a checklist and a severity rubric in references/, and a script that runs the repository’s own checks before anything is reported.
What it still cannot do is refuse. SKILL.md can say never run git push --force, and the model will usually oblige — but nothing enforces it, because text is advice and only code running inside the Pi process can block a call. Chapter 15 starts there.