Your users keep skills as SKILL.md folders, in the repo and in their home folder, and they want to call them from your agent like any other command. The skills plugin reads those folders, gives the model the list of skills, and makes each skill a /<name> command. A skill that a user adds while the session runs works at once.
The plugin reads the folders with node:fs, so it runs on Node.js. It does not work on Cloudflare Workers.
npm i @tanstack/ai-skillspnpm add @tanstack/ai-skillsyarn add @tanstack/ai-skillsbun add @tanstack/ai-skillsimport { homedir } from 'node:os'
import { join } from 'node:path'
import { defineHarness } from '@tanstack/ai-harness'
import { openaiText } from '@tanstack/ai-openai'
import { skills } from '@tanstack/ai-skills/harness'
// One time, outside `plugins`, so the sessions share the folder watchers.
const agentSkills = skills({
dirs: ['./.agents/skills', join(homedir(), '.acme', 'skills')],
})
export const assistant = defineHarness({
name: 'acme/assistant',
adapter: openaiText('gpt-6.1-sol'),
plugins: () => [agentSkills],
})/release-notes write them for v2 starts a turn with "Use the release-notes skill: write them for v2". The model loads the skill with load_skill, then does the task. /release-notes alone asks for the skill with no task.
The model also sees the list of skills, so it can load one on its own when a task matches.
A skill with the name of another command gets a prefix: a model skill is /skill:model, and /model stays the model picker. Pass reserved with the names that your UI handles itself:
export const cliSkills = skills({
dirs: ['./.agents/skills'],
reserved: ['help', 'exit'],
})/skills lists every skill, its folder, and its command.
The plugin watches the folders, and the commands follow them:
Each change sends a harness.commands.changed event, so a UI can show the new command list at once.
The plugin watches each folder once for each host. All sessions of the harness on that host share the watchers, so many open sessions still use one watcher for each folder. A change only tells the sessions to update. The plugin reads the folders again at the start of each turn.
If the plugin cannot watch a folder, it logs one warning with the folder and the reason. An example is a Linux system with no free file watchers. Changes in that folder still show at the next turn.
The model's list has one line for each skill: the name and the first sentence of the description. When the list is longer than maxCatalogTokens (default 4000), the skills of the earlier folders stay, and the rest leave the model's list. A skill that the user calls with its command always stays in it.
The other options of withSkills pass through, for example maxCatalogTokens and requireApproval:
export const manySkills = skills({
dirs: ['./.agents/skills'],
maxCatalogTokens: 8000,
})Next: Agent Skills has the SKILL.md format, and Write a harness plugin shows the ctx.commands API that the plugin uses.