Skills
A skill is a directory of instructions, scripts and reference files that an agent can load when a task calls for it. rupu reads the open Agent Skills format, so a skill written for another harness installs and validates here unchanged.
Layers
Skills live in two places:
| Layer | Directory |
|---|---|
| global | ~/.rupu/skills/<name>/ (or $RUPU_HOME/skills/) |
| project | <repo>/.rupu/skills/<name>/ |
A project skill does not shadow a global one of the same name. Both are kept, and the grant decides which one
an agent gets. When the repo has no .rupu/ of its own, its project root resolves to
$HOME, and ~/.rupu is then the global layer only, never both.
.claude/skills,
.agents/skills or .codex/skills. Scanning them would let a repo
under assessment plant instructions and scripts that rupu's agents then run. Copy a skill in on purpose with
rupu skill install.
The SKILL.md format
A skill is a directory whose name is the skill's name and which contains SKILL.md:
~/.rupu/skills/report-style/ ├── SKILL.md ├── scripts/render.sh └── references/STYLE.md
SKILL.md starts with YAML frontmatter. rupu enforces these rules:
| Field | Rule |
|---|---|
name | Required. 1 to 64 chars, a-z, 0-9, -, no leading, trailing or doubled -. Must equal the directory name. |
description | Required, not empty. Over 1024 chars is a warning; the index the model sees cuts it at 1024 chars. |
license | Optional, free text. |
compatibility | Optional, not empty when present. Over 500 chars is a warning. Shown by rupu skill show. |
metadata | Optional string-to-string map. Kept, not interpreted. A number such as version: 1.0 is read as the string "1.0". |
allowed-tools | Optional. A space-separated string, or a YAML list. Informational only: it is never a tool grant. |
disable-model-invocation | When true the skill is left out of the index the model sees. |
| anything else | Accepted and ignored, since other harnesses add their own keys. |
The 1024 and 500 limits come from the agentskills.io specification. Published skills go over them, so rupu warns rather than rejecting the skill. The body after the frontmatter is the instructions; paths in it are relative to the skill's directory. A UTF-8 BOM and CRLF line endings are accepted.
A skill is invalid when its frontmatter breaks a rule above, or when a symlink inside it does not resolve or resolves outside the skill directory. In the global layer the skill directory itself may be a symlink. In the project layer it may not, so a repo cannot point a skill at an arbitrary directory on the machine. Invalid skills are listed with their error.
Versions
rupu fingerprints a skill two ways. The stamp hashes each file's path, size, modification time
and mode, and costs one stat per file. The content hash reads every
byte and is computed only when a skill is loaded. Both skip .git and
.DS_Store at any depth, and both count a file's mode only as executable or not.
Because the stamp reads no bytes, it can miss a same-size edit whose modification time was put back (for
example by cp -p or touch -r).
Installing
rupu skill install <source> [--ref <r>] [--skill <name>]... [--all] [--list] [--project] [--force]
The source is whatever you would copy from a README or a browser:
| Source | Resolves to |
|---|---|
an existing path (./x, /abs/x, ~/x) | that local directory |
owner/repo | https://github.com/owner/repo |
https://github.com/o/r/tree/<ref>/<path> (or /blob/<ref>/<path>/SKILL.md) | that repo, ref and path |
any https:// git URL, with an optional #<path> | that repo, and the folder at that path |
In a GitHub tree URL the ref is the first segment after tree/, so a branch with
/ in its name needs --ref and #<path>
instead. Only https:// is fetched: ssh://,
git@ and file:// are refused, and a local checkout is
installed as a path. The fetch is a shallow clone of the default branch, or a full clone checked out at
--ref. No git hooks run and submodules are not fetched. Private repositories are not
supported yet, and a URL with credentials in it is refused so the token never lands in the source record:
clone the repository yourself and install from the directory.
rupu searches the source for every directory holding a SKILL.md, through hidden
folders such as .claude/skills/, up to 6 levels deep, skipping
.git and node_modules. Then:
- One skill found: it is installed.
- Several found:
--skill <name>(repeatable) picks some,--alltakes every valid one and reports the invalid ones it skipped, and with neither the command lists them and installs nothing. --listlists the skills in the source and installs nothing. An invalid skill's row readsINVALID: <error>in place of its description.
rupu skill install anthropics/skills --list rupu skill install anthropics/skills --skill pdf
The other flags:
--ref <branch|tag|commit>installs from that ref. It conflicts with a different ref in a tree URL.--projectinstalls into the current project's.rupu/skills/instead of~/.rupu/skills/. Outside a rupu project it is refused.--forcereplaces an installed skill of the same name. Without it the install is refused.
Every chosen skill is validated before anything is written, and a skill containing an absolute symlink is
refused. Each copy keeps its file modes, so scripts stay executable, and is swapped in by rename, so a failed
install, --force included, never damages the installed skill. When an install of
several skills fails part way, the skills installed before the failure stay installed, with their source
records.
A skill's scripts need their own tools on the machine: a skill that runs python3 scripts/render.py
needs python3 wherever the agent runs, including any remote host a placed unit runs on.
Updating
Each install writes a source record to <layer>/skills/.sources/<name>.json,
outside the skill's directory so it never changes the skill's fingerprints. It holds what you typed, the
resolved URL, ref and path (or the local directory), the commit, and the content hash of what was installed.
rupu skill update report-style rupu skill update --all [--project] [--force]
update re-fetches each skill from its record, cloning each URL and ref once, and replaces the installed copy. It prints one line per skill:
| Result | Meaning |
|---|---|
updated <old> → <new> | the source changed and the new copy is installed |
up to date | the source matches what is installed |
no source record | the skill was copied in by hand; reinstall it to track a source |
locally modified | the installed copy no longer matches its record, so someone edited it; --force replaces the edits |
If the repository moved the skill, update finds it by name and the record follows it.
A skill installed at a pinned --ref stays on that ref; moving it is a new
rupu skill install --ref. update exits 1 when any skill was
locally modified, missing or failed to update. There is no remove: delete the skill's
directory; an orphaned source record is ignored.
Listing, showing and validating
rupu skill list [--project] rupu skill show report-style rupu skill show project:report-style rupu skill validate report-style rupu skill validate --all
list shows every skill in both layers with its layer, status, source and description,
or its error when it is invalid (--format json|csv supported).
show prints the frontmatter, the source record, the stamp and content hash, and the
skill's files; a bare name shows every layer that has it, and the global: or
project: prefix picks one. validate checks skills against the
rules above and exits 1 when any is invalid; it prints a warn line for each warning,
and warnings alone exit 0.
Granting skills to agents
Installing a skill makes it available; it does not give it to any agent. An agent names the skills it gets in its frontmatter:
skills: [pdf, code-review] # the model may load these preloadSkills: [house-style] # always in the system prompt
* grants every valid global skill and never a project one; a project skill needs its
exact name or project:*. A bare name installed in both layers is an error until it is
written global:<name> or project:<name>. An unknown
or invalid skill fails the agent's load, and rupu agent validate catches it. The
frontmatter keys are described with the rest in Agents.
Skills on workflow steps
A workflow step can give skills to the agents it launches, on top of each agent's own:
steps: - id: review agent: reviewer prompt: Review the PR. skills: [pdf, "project:*"] # the model may load these preload_skills: [house-style] # always in the system prompt
The entries use the same grammar as an agent's skills:. A step's skills only add: they
never remove one the agent declares. They reach every agent the step launches: its own agent, each
for_each unit, the members of a parallel: block, and a panel's
panelists and its gate's fix_with fixer. A sub-step has no skills:
of its own; it takes its parent step's.
The keys are allowed only on a step that runs an agent. A gate, action:,
run:, branch:, split: or
join: step that names them fails to parse. Parsing checks each entry's syntax only;
whether the skill is installed is checked when the step launches, so a missing skill fails that step before
its agent runs, the way a missing agent does. The run's skill_index records step
skills with reason launch, the same as rupu run --skill. A
placed step (host: or distribute:) sends its skills with the
unit — see Skills on placed units.
How an agent uses its skills
Every run the agent starts (rupu run, a workflow step, an agentiflow lead or unit) gets its skills the same way:
- Index or preload. A skill in
skills:is listed under## Skillsat the end of the system prompt: its name and description, about a line each. A skill inpreloadSkills:has its whole body in the system prompt instead. The section is rendered the same way for the same skills, so the prompt cache holds across runs. skills.load. When a task matches a listed skill, the model callsskills.loadwith its name and gets its instructions, base directory and file list. The skill is read from disk at that moment: one edited since the run started loads at its new version, with askill_changednotice. The model can load only skills the agent was granted.- Reading and running its files.
read_file,grep,globandast_grepreach a granted skill's directory, even outside the workspace. A skill's files are read-only to the model:write_fileandedit_filerefuse them. Its scripts run throughbash, under the run's permission mode (readonlydenies them,askprompts) and[bash]settings. A skill never adds a tool or raises the permission mode. - Compaction. The runner keeps the skills the model loaded and puts them back at their current version, in the rebuilt first message, after every compaction. They are capped at a quarter of the model's input limit, most recently loaded first; a skill that doesn't fit is named, for the model to load again. A skill uninstalled mid-run is named as missing.
- Continuing a run.
rupu run --continueand a resumed workflow step start with the skills the interrupted run had loaded, and re-attach them at their next compaction. An agentiflow lead carries them from round to round.
rupu run --skill <name> and --preload-skill <name>
(repeat the flag for several) give one run skills on top of its agent's; its skill_index
records them with reason launch. rupu run --continue restores
the earlier run's launch skills with the rest of its command; --skill on the continue
replaces the whole skill list and --preload-skill the whole preload list.
Invoking a skill with /name
A prompt that starts with /name invokes that skill, as /skill-name args
does in Claude Code:
rupu run reviewer "/pdf summarise q3.pdf"
Each leading / token is an operator entry (/pdf,
/project:pdf) and the rest of the prompt follows the last one. The skill joins the run's
launch skills (recorded as a --skill in its command, so --continue
keeps it), its body goes ahead of the rest of the prompt, and the transcript records a
skill_load with via: operator. The first token that names no
installed skill starts the prompt as text, so /tmp is full and
/etc/hosts are sent unchanged. An ambiguous name or one named from both layers fails
the run. rupu session start and rupu session send read the same
tokens.
A child started with dispatch (any kind, or a legacy name like
dispatch_agent) gets its own agent's skills, not its parent's.
The transcript records what the run had and used: skill_index lists the granted skills
at run start, and a skill_load is written for every load, with via
set to model, preload, reattach or
continuation. Notices name what needs attention: skill_not_indexed,
skill_changed, skill_missing,
skill_tools_missing and skill_reattach_truncated. See
Transcript schema.
Skills in sessions
A session fixes its skills when it starts. rupu session start resolves the agent's
skills: and preloadSkills: once, against the global layer and
the session's project layer, and stores the set and the rendered skills section on the session. Every turn
sends that same section, so the system prompt is byte-identical from turn to turn and the prompt cache holds.
A preloaded skill's body is the one it had at start; a session that wants its newer version is a new session.
A skill installed after the session started is not offered, even under *. A skill the
model loads is read at its current version, and one edited since the session started loads with a
skill_changed notice. A skill uninstalled mid-session is left out of each turn with a
skill_missing notice, and the session keeps running.
rupu session start --skill <name> and --preload-skill <name>
(repeatable) add skills to the session on top of its agent's; its skill_index records
them with reason session.
rupu session send --skill <name> <id> [prompt] (repeatable) invokes a skill in
that turn. It takes any valid skill installed in the session's global or project layer, granted or not,
including one marked disable-model-invocation, which exists for exactly this. A bare
name installed in both layers, a wildcard, an unknown name (with a did-you-mean) and a name the session holds
from the other layer are refused before anything is sent. The prompt is optional when a skill is given. The
skill's body goes ahead of the prompt in the turn's message:
<skill name="pdf" base="/Users/me/.rupu/skills/pdf" version="3f2a9c0d41b7"> ...the skill's instructions... </skill> summarise x.pdf
A prompt that starts with /name does the same as --skill name
(rupu session send <id> "/pdf summarise x.pdf"), after any --skill
entries; rupu session start takes /name in its first prompt the
same way. version is the first 12 hex digits of the skill's content hash. The turn's
transcript records a skill_load with via: operator. From then on
the skill is part of the session with reason operator: the model may load it again with
skills.load and read its files. It is never added to the index, so the system prompt
does not change.
The skills the model loaded survive turns and compaction: every compaction puts them back at their current
version — the runner's own compaction during a turn, rupu session compact, and the
compaction a running session's worker does on request. A loaded skill uninstalled for a while is named as
missing and stays in the set, so it comes back once it is installed again.
rupu session list shows such a session
with status newer rupu, and rupu session prune leaves it alone.
A queued turn that invokes skills is turn request version 2, and a worker that knows --skill
fails a turn queued by a newer rupu with a transcript saying so. After upgrading rupu, run
rupu session stop <id> before using --skill on a running
session. More in Sessions.
Skills on placed units
A placed unit runs rupu run <agent> on another host. The coordinator ships the
skills the unit needs, and the host runs the unit with exactly those copies. The host does not need them
installed.
- What ships. The coordinator resolves the unit's skills the way a local run does: the
skills:/preloadSkills:of its own copy of the agent, plus the step'sskills:/preload_skills:.*means the coordinator's global layer. If a step skill is not installed on the coordinator, the unit fails before it is dispatched. If the coordinator has no copy of the agent, or its file does not load, only the step's skills ship, with a warning in the second case. - What the host does. Each skill is packed as a tar and named by its content hash. The coordinator asks the host which hashes it lacks and sends only those. The host checks every tar before keeping it — no absolute or
..paths, no symlink that leaves the skill, only files, directories and symlinks, and the unpacked tree must hash to the name it was sent under — and keeps the copy in<RUPU_HOME>/cache/skills/<hash>/<name>/(mode 0700). The cache has no eviction. - Overlay. A shipped skill replaces the host's installed skill with the same layer and name, so the coordinator's version wins. The host still loads the agent itself and resolves its grants over its global layer, its staged project layer and the shipped skills; the rules do not change. The step's own entries go to the host as the
layer:nameentries they resolved to on the coordinator. The unit'sskill_indexmarks shipped skills(shipped). - Limits. A skill tree may be at most 32 MiB and one unit's bundle at most 128 MiB. The coordinator refuses a unit over either limit before dispatch and names the skill and the limit.
- Transfers. Within one coordinator process, a host that confirmed a hash is not asked again, and the units of a fan-out bound for one host share one transfer. Over SSH a transfer is two commands per host (
rupu __skills missing,rupu __skills receive). Over HTTP the peer servesPOST /api/skills/bundle/missingandPUT /api/skills/bundle/:hash. Tunnel nodes takeSkillsOffer/SkillsNeed/SkillChunk/SkillsStoredframes; a bucket host gets each missing skill asskills/<hash>.tar, and the worker downloads and verifies what its cache lacks before the job starts.host: localships nothing. - Older hosts. A host that does not advertise
agent.skill_bundleis sent the unit by name, as before, and the run gets a warning that names the host and says the unit will use the host's installed copies. Over SSH and HTTP the coordinator remembers the downgrade for the life of its process; for tunnel and bucket the capability is re-checked on every unit. - Errors. Every coordinator-side failure for a placed unit's skills (resolve, pack, limit, transfer) and a skill missing on the host starts with
host <id>:.
Only the placed unit's own skills ship. Children the unit launches on the host, a rupu coverage
rerun, and an agent whose own grants the coordinator did not ship still resolve from the host's installed
layers. On the host, the hidden rupu run --skill-bundle <json> carries the manifest
and is recorded with the rest of the command, so rupu run --continue runs with the same
shipped skills. The transports are described in Multi-host.
In the control plane
The control plane shows installed skills and edits where they are granted; installing and updating stay CLI
operations. The Library's skills tab lists the global layer, and the project picker adds that project's layer,
like rupu skill list. Each row shows its layer, status, source and description; an
invalid skill shows its error. A layer that can't be read is left out with a warning, and the other layer still
lists. A skill's page shows its description, compatibility, license, allowed-tools with
the rupu tools they map to (informational, never a grant), metadata, files, body, content hash and install
record.
An agent's skills: and preloadSkills: are edited in the agent
builder's Skills card, with the installed names as suggestions. The control plane checks an agent's skills the
way rupu agent validate does; an agent that fails shows "This agent can't be launched"
with the reason, and an unknown name comes with the install hint. Saving such an agent is allowed, so an agent
can be written before its skills are installed. A workflow step's skills and
preload_skills are fields of the step form for any step that runs an agent. The
launcher's Skills section adds skills to a run or a session, and the session composer and the launcher's
prompt open a menu of the installed skills when a message starts with /. A remote host
gets no menu, since this control plane can't list its skills; a typed /name still
reaches a rupu there that reads / tokens. Session skills on a remote host need a rupu
there that advertises session.skills; run skills need agent.skills.
The routes are in the HTTP API.