Skip to content
Beta. VectorStep is in beta.Let us know if something breaks.

Skills

An agent’s capabilities today are tools: (MCP servers it can call) plus whatever soul.md tells it to do, always loaded on every call. A skill is a third thing: a self-contained, reusable body of instructions — “how we triage a Sev2,” “how to write a Terraform module our way,” “the runbook for rotating this credential” — that an agent discovers and loads only when a conversation actually needs it, rather than paying for in every system prompt regardless.

Skills live as subdirectories under skills_dir (/data/skills in the container, ~/.vectorstep/skills/ from the host side — same shape as agents_dir, see Creating agents).

skills/
incident-triage/
SKILL.md
scripts/
classify_severity.py
reference/
escalation-matrix.md
---
name: incident-triage
description: >
Use when the user is reporting a production incident and needs help
classifying severity, identifying the right escalation path, and drafting
an initial status update. Covers Sev1-Sev4 per this org's matrix.
---
# Incident Triage
1. Ask for: affected service, user-visible symptom, start time.
2. Classify severity using `reference/escalation-matrix.md`.
...
Field Required Description
name Yes Must match the directory name — same rule and same reason as an agent’s name vs. its directory.
description Yes The only field an agent pays context for when the skill isn’t in use — see Writing a good description below.

The Markdown body is the skill’s full instructions, loaded only when the agent decides the skill is relevant. Any other file in the skill’s directory — a reference doc, a template, a script — is read individually, by path, only when the agent asks for it. Nothing beyond SKILL.md is ever preloaded into context.

This is the same three-level shape a well-designed tool-search or documentation system already uses — cheap metadata always present, full content fetched on demand, bundled resources fetched individually past that:

  1. Level 1 — always in the system prompt, for every granted skill. One line per skill: name and description. This is the entire cost of a grant an agent isn’t currently using.
  2. Level 2 — the full SKILL.md body, loaded on demand. The agent calls a synthetic load_skill(name) tool when a skill’s description looks relevant; it gets back the full instructions plus the list of any bundled files.
  3. Level 3 — one bundled file at a time, read on demand. The agent calls read_skill_file(skill_name, path) for a specific reference doc or template load_skill told it about.

An agent with no skills: grants pays nothing for this feature at all — not the catalog text, not the two tool schemas. Both synthetic tools are added to the tool list only when at least one skill is granted.

Default-deny, identical posture to tools: — an agent sees zero skills unless its agent.yaml names them:

name: sre-triage
model: anthropic/claude-sonnet-4-6
max_tokens: 8192
skills:
- incident-triage
- terraform-module-style

There’s no scoping-within-a-skill the way tools: can scope down to specific tool names — a skill is granted or it isn’t.

description is the one field most likely to be gotten wrong by someone used to always-loaded system prompts: it’s the only thing that costs context when the skill isn’t in use, and it’s the only signal the agent has to decide whether to load the full body at all. Write it like a router, not a summary — state plainly when to use this skill, not just what it contains:

  • Good: “Use when the user is reporting a production incident and needs help classifying severity, identifying the right escalation path, or drafting an initial status update. Covers Sev1-Sev4 per this org’s matrix.”
  • Weak: “Incident response skill.” — doesn’t tell the agent what situations should trigger loading it, or what’s actually inside.

Skills are authored the same way agents are — hand-edit the files on disk, or use the REST API / Gateway MCP (create_skill, update_skill, delete_skill, validate_skill). See REST API for the full endpoint set. skills/ is gitignored, same as agents/ — every write result says so explicitly, never that anything was committed.

Terminal window
POST /reload # via HTTP
kill -HUP <pid> # via SIGHUP

Reloads all skill directories from disk without restarting, in the same call that reloads agents — not a separate reload path to remember. In-progress runs are unaffected.

A skill can bundle a script (scripts/classify_severity.py above) as ordinary content — installable via create_skill/update_skill like any other bundled file. That’s deliberately not the same thing as making it runnable.

Content is API-writable. Execution approval never is — full stop. A skill’s script only becomes callable if an operator separately, manually adds it to security.skills.allowed in config.yaml:

security:
skills:
allow_script_execution: false # default false — off by default, same
# posture as VectorStep's shell steps
allowed:
- skill: incident-triage
path: scripts/classify_severity.py # relative to the skill's own directory
sha256: 3f2a1c9e... # required — no unpinned option

config.yaml is loaded once at startup and is not writable via any API — that’s what makes this an operator control an admin-token caller can’t loosen through the REST API or Gateway MCP, no matter what it can do to a skill’s content. An admin-token caller can ship a script as part of a skill’s bundled files; only someone with host/filesystem access to config.yaml can make that specific script executable. POST /reload / SIGHUP reload skill content but never re-read config.yaml, so approving a new script needs an actual restart.

On Kubernetes, where skills arrive from a ConfigMap, see Skill scripts for how to change an approved script and its pin together.

When enabled and approved, the script is offered to the agent as a third synthetic tool, run_skill_script(skill_name, path, args) — but only if this specific agent has at least one granted skill with at least one approved script; an agent with nothing runnable never sees the tool at all. Arguments are passed literally as argv entries, never shell-interpreted — there is no free-text command field anywhere in this feature. The script’s checksum is re-verified immediately before every single run, not just at startup, so a script swapped on disk after boot is caught rather than silently executed. Execution is contained the same way VectorStep’s own executor: shell pipeline steps are: an isolated scratch working directory, CPU/memory/output-size limits, and a killed process group on timeout — see Executors — shell for the shared design this was ported from. The subprocess runs with an empty environment — nothing inherited from the gateway process.

GET /skills / GET /skills/{name} include an executable_scripts field — which of a skill’s bundled files are currently approved — a read-only fact safe to expose even though the approval list itself isn’t.

VectorStep has a read-only Skill Library (/ui/skills) that browses whatever’s loaded on the Gateway — description, bundled files, which agents are granted each skill, usage over time, and a version history that survives a skill’s content changing or the skill being deleted entirely. A run’s step detail shows which of its agent’s granted skills were actually loaded, and Insights — Skills rolls up load rate and script activity across every agent that runs it — the fastest way to tell whether a skill’s description is actually getting it triggered. None of this is writable from VectorStep; it’s read-only observability over what you author here as files.

  • Creating agents — skills: sits alongside tools: in agent.yaml; see Writing good agents for when a capability belongs in one vs. the other.
  • Tool policy — the operator-owned allow/deny backstop for tools: calls does not currently extend to skills; an agent’s skills: grant is the only gate today.
  • REST API — the full /skills* endpoint set.
  • Configuration — skills_dir and security.skills.