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:

LayerDirectory
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.

Not scanned. rupu does not read .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:

FieldRule
nameRequired. 1 to 64 chars, a-z, 0-9, -, no leading, trailing or doubled -. Must equal the directory name.
descriptionRequired, not empty. Over 1024 chars is a warning; the index the model sees cuts it at 1024 chars.
licenseOptional, free text.
compatibilityOptional, not empty when present. Over 500 chars is a warning. Shown by rupu skill show.
metadataOptional string-to-string map. Kept, not interpreted. A number such as version: 1.0 is read as the string "1.0".
allowed-toolsOptional. A space-separated string, or a YAML list. Informational only: it is never a tool grant.
disable-model-invocationWhen true the skill is left out of the index the model sees.
anything elseAccepted 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:

SourceResolves to
an existing path (./x, /abs/x, ~/x)that local directory
owner/repohttps://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:

rupu skill install anthropics/skills --list
rupu skill install anthropics/skills --skill pdf

The other flags:

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:

ResultMeaning
updated <old> → <new>the source changed and the new copy is installed
up to datethe source matches what is installed
no source recordthe skill was copied in by hand; reinstall it to track a source
locally modifiedthe 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:

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.

Record versions. A session record that carries skills is written as version 2; one without skills stays version 1. A rupu that knows session skills refuses a record from a newer rupu rather than write it back without the fields it doesn't know; 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.

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.