013 · Skills
Skills, the skills policy and skill scripts (spec_skills_013)
| Status | Implemented. Reverse-engineered from 53f8c53 (2026-09-24) and kept with the code since; its paths and names checked 2026-09-27 (//tools/specs). |
| Source | pkg/engine/skills/{skill,definition,policy,provider,folder,save}.go, pkg/engine/{skillfiles,skillmentions}.go, apps/service/internal/server/skills_edit.go, apps/desktop/web/src/studio/SkillsPane.tsx, SkillFiles.tsx, CodeArea.tsx, skillForm.ts, apps/desktop/web/src/skillMention.ts, pkg/engine/skills/builtin/*; pkg/engine/tools/{skills_tools,skillscript,pyenv,scriptbox,scriptbox_gvisor_linux}.go |
| Tests | pkg/engine/skills/*_test.go (save_test.go: kinds, names, saving, files); pkg/engine/skillfiles_test.go; apps/service/internal/server/skills_edit_test.go; apps/desktop/web/src/studio/skillForm.test.ts, skillMention.test.ts; pkg/engine/tools/skills_tools_cov_test.go (meta skills, agents, references), skillscript_test.go (TestRunShellScript); pkg/engine/tools/skillscript_test.go, pyenv_test.go, scriptbox_test.go, scriptbox_gvisor_linux_test.go; apps/cli/internal/tui/skills_test.go, envs_test.go |
| Depends on | spec_config_002, spec_approvals_005, spec_shell_007 (OS sandbox) |
Skills are reusable instructions (SKILL.md) an agent loads on demand, optionally shipping scripts. Frontmatter follows the Agent Skills fields plus Castor’s castor.skills.v1.SkillDefinition (proto field names). A host skills policy caps what any skill may do; the stricter of the skill’s request and the policy always applies. Scripts run only sandboxed, read the workspace but never write it.
- SK-01 Built-in skills are embedded (
code-review,git-workflow,testing-tdd). External skills areSKILL.mdfiles underskills.paths:~/.blitz/skills, and the project’s./skillsand.agents/skills, whose scripts run only once the project’s settings are trusted (spec_project_config_031). - SK-02 External skills cannot replace built-ins (reported). Parse errors are reported (a typo must not make a skill vanish silently) while other skills still load. Neighbouring resource files are indexed.
- SK-03
Listis sorted by name;Searchmatches name, description or tags.
- SK-10 Agent Skills fields:
name,description,license,compatibility,allowed-tools,metadata. Castor fields:tags,version,author(s),allowed_tools(legacy),tool_requirements[{name, scopes, description}],category,trigger_phrases,execution_hints{preferred_model, requires_human_approval, environment_variables, timeout_seconds, custom_hints, hitl_tier, allow_hitl_bypass},compiled_reference,scripts[],skill_id,uri,source_uri. Unknown keys are ignored. - SK-11 Script:
name(letters, digits,. _ -, unique),language(python/typescriptor proto enum names; required), exactly one ofinline_code,storage_uri(https, orgs://bucket/object; withstorage_sha256, SK-49),relative_path(must stay inside the skill directory),entry_point,dependencies,timeout_seconds ≥ 0,environment_variables(valid names). - SK-12 HITL tiers by name only (
HITL_POLICY_TIER_2_AUDITED_WRITE,TIER_2_AUDITED_WRITE,tier_2); bare numbers are refused (the proto enum is offset by one). Declared tier =hitl_tier, else the compiled reference’s, and at least tier 3 whenrequires_human_approval. - SK-13 Network need:
custom_hints.network=true/required(no proto field yet). - SK-14 A skill with definition problems still loads (instructions are useful) but its scripts don’t run.
- SK-15 Content hash: SHA-256 over every regular file in the skill directory in path order (relative path, size, content), symlinks skipped, format
sha256:<hex>, bounded size.
- SK-20 Blocks for all scripts: definition problems; unhashable content;
trusted_hashesset and the hash not listed; a tool requirementnameorname:scopematchingdeny_toolsglobs; network needed but not granted (granted only withnetwork = "allowlist"and the skill innetwork_allow). - SK-21 Tier: stricter of declared and
min_hitl_tier(0 or 1 → tier 1; capped at 3; default 2). Tier 0 (bypass) requires both the skill’sallow_hitl_bypassand the policy’s; otherwise it becomes tier 3. - SK-22 Environment: variables the skill asks for pass only if they match
env_passthroughglobs; the rest are withheld (reported). - SK-23 Per script: language must be in
languages(defaultpython,typescript); timeout = script’s, else hints’, capped bymax_timeout_seconds(300) which also applies when none is set; each dependency must be a plain PEP 508 requirement (no URLs, paths or pip options), not inpackages.deny, inpackages.allowif set (PEP 503-normalised names, globs), and pinned==whenrequire_hashes. - SK-24
Problems()of the policy are warned at startup and bydoctor.
- SK-80 Custom slash commands (
pkg/engine/commands): Markdown files in~/.blitz/commands/(user) and the workspace’s.blitz/commands/,.claude/commands/,.agents/workflows/(project, which win; a.flow.yamlthere is a workflow, spec_workflows_043, run by the same/name; prompt text, so no trust is needed — spec_parity_027 §12.3), read on each use. The name is the path without.md, subdirectories as:namespaces, lower case ([a-z0-9][a-z0-9._-]*). Optional frontmatter:description(else the body’s first line),argument-hint,agent,model,allowed-tools(a list or a comma/space-separated string;Bash(git *)stays one entry),mode: plan; unknown modes and empty bodies are errors, reported once. - SK-81 Expansion:
$ARGUMENTSis everything typed after the name,$1–$9its words; without either, the arguments are appended. The turn (Turn.Command) sends the expanded prompt and records the line as typed;agent/modelapply to that turn only (ENG-16),allowed-toolslimits it (runtime.WithAllowedTools; Claude Code names map to Blitz tools,Bash(…)counts as the shell tool),mode: planplans it.ErrUnknownCommand/UNKNOWN_COMMANDotherwise. - SK-82 Every skill is a command
/<skill-name> <request>, whose prompt carries the request and the skill’s instructions (bundled and file commands of the same name win). Bundled commands:/reviewand/security-review(plan mode: read-only reviews of the uncommitted changes, a branch or files, verified findings with file and line),/simplify(behaviour-preserving cleanup of the changed files, then tests),/verify(find and run the project’s build, lint and test commands and report),/grill-me <task>(plan mode: the agent asks clarifying questions withask_user_question, one at a time, until what’s left wouldn’t change the approach, then proposes one; it writes nothing, and the transcript records/grill-me <task>; BL-CLI-02),/setupand/spec(the harness and the specification, spec_memory_012 MEM-22, MEM-42),/skills-updateand/agents-update(MEM-44;/skills updateand/agents updateare the same,commands.Alias). Built-in REPL commands win name clashes;/helplists the custom ones with their source;ListCommandsserves them.
- SK-30
list_or_search_skills(query optional) → name, description, tags. - SK-31
activate_skill(skill_name)→ the full instructions and resources, plus each script with description, whether the policy allows it, and why not. - SK-32
run_skill_script(skill, script, args?)→exit_code, stdout, stderr, timed_out, sandbox, tier, output_dir, output_files, error. Registered only when skills are enabled. - SK-33 Engine: when skills exist, the root agent’s instructions list them and suggest
activate_skill(spec_engine_016 ENG-02).
- SK-40 The script must be allowed by the policy. Approval by tier: tier ≥ 3 asks every time with no key (not rememberable); tiers 0–2 run without asking and are audited (
auto-tier_n). - SK-41 Dependencies: an isolated environment per distinct (python, normalised deps, index, wheels-only) in
~/.blitz/envs/<key>. Installing needs its own approval showing the exactpip install --index-url … [--only-binary :all:] -- depscommand, rememberable (keypyenv:<key>) for exactly that list. Built inside the script sandbox with the network on and writes only to the environment and package cache, withuvif installed elsevenv+pip, frompackages.index(default PyPI), wheels only by default. The environment is used only after its marker file is written (an interrupted build is rebuilt). The system Python is never modified. Build time is bounded. - SK-42 Environment passed:
PYTHONDONTWRITEBYTECODE=1,PYTHONNOUSERSITE=1,SKILL_DIR, the policy-allowed variables, and at tier ≥ 2 (or bypass)SKILL_OUTPUT; plus a fixed PATH, private HOME and TMPDIR, LANG. Nothing else is inherited. - SK-43 Writes: tier 1 — nothing but its private
/tmp; tier ≥ 2 and bypass — only.blitz/skill-output/<skill>/<run>/in the workspace. The workspace is readable. The agent reads the results there and applies changes with the file tools (so diffs, approvals, checkpoints and/undowork). Output files are listed in the result. - SK-46 TypeScript (BL-SK-01): the script runs on the system
node, 22.6 or later (SystemNode; else refused with the reason), with its types stripped (--experimental-strip-types --no-warnings), from a temporary copy of the skill (or the inline code as<name>.ts) so it can import its neighbours. Its npm dependencies go into an environment per distinct set in~/.blitz/node-envs/<key>(NodeEnvs), built as Python’s are (SK-41) withnpm install --ignore-scripts(no package’s code runs at install) from[skills.policy.packages.npm] registry, approved once per exact list (keynodeenv:<key>); anode_moduleslink beside the copy finds them. Each dependency must be a plainnameorname@version(scoped names too; no tarballs, git, file or alias specs), innpm.allowwhen set, not innpm.deny, and withnpm.require_exactan exact version.entry_pointimports the module and calls the named export; a number it returns (or resolves to) is the exit code. The Python-only variables aren’t set; the sandbox, writes, variables and timeout are as for Python./envslists Python’s environments only. - SK-47
execution_hints.writes_workspace: true(BL-SK-02): the skill’s scripts run in a copy of the workspace (its working directory; not.git, blocked paths, symbolic links or files over the size limit; refused past 512 MiB or 50,000 files). When the script succeeds, what it changed, added or deleted in the copy is shown as one diff in an approval asked every time and never remembered, as for tier 3 (MustAsk, kindwrite); approved, it is written through the workspace (WriteFileAtomic,RemoveFile), so it joins the turn’s checkpoint and/undoreverts it, andchanged_fileslists it. Refused, or when the script fails or times out, nothing reaches the workspace.activate_skillsayswrites_workspace. The script’s tier approval (SK-40) still applies before it runs. - SK-48 A Python script’s interpreter requirement (BL-SK-03): its definition’s
requires_python, elserequires-pythonin its PEP 723 metadata (# /// script…# ///), a PEP 440 specifier set of release versions (>=3.11,<3.14,~=3.11,==3.12.*). The systempython3runs it when it meets it. Otherwise, withuv(onPATHor in~/.blitz/bin), an interpreter uv finds for it (uv python find, never downloading) does; when there’s none, one is installed after an approval rememberable per requirement (keyuvpython:<spec>), withuv python installinside the script sandbox (network on, writes only to uv’s Python directory and cache). Without uv the script is refused, saying which Python it needs and which the system has. Environments (SK-41) are per interpreter, so each gets its own. - SK-49
storage_uriscripts (BL-SK-04):https://…as it is,gs://bucket/objectthroughhttps://storage.googleapis.com/bucket/object(public objects). The definition must pin the content withstorage_sha256(hex; Blitz’s field, since Castor’s proto has none; SKILL.md’s content hash, and sotrusted_hashes, covers it). A run fetches it under the web rules, asweb_fetchdoes:[web]enabled and the sandbox’s network, public addresses only (unlessallow_private),deny_domains, and approval per host (web:<host>) unlessallow_domainsor an allow rule covers it; at most 10 MiB, no redirect to another host. A wrong hash refuses the run. The content is kept by its hash in~/.blitz/skill-cache, so later runs don’t fetch; then it runs as inline code would. - SK-34
activate_skillalso lists the skill’s Castorresourcesasassets: id, name, description, category (document_text, …), MIME type, size, a URL to fetch (the public URL, else thegcs_urithrough Cloud Storage’s endpoint), inline content, the interpretation’s summary, and its text whenauto_inject_contextis set (each at most 16 KiB). - SK-44
entry_point: the named function is called (module not run as__main__) and its return value is the exit code. - SK-45 Stopped at its timeout or on Ctrl+C.
- SK-50
gvisor(Linux): each run in its own rootless gVisor sandbox (runscfrom PATH,~/.blitz/binorRUNSC_PATH, beside itsgvisor-bin/); root is empty, only host binaries/libraries,/etcand the request’s paths are mounted,/tmpis a private tmpfs, home directories don’t exist, network off unless allowed. Probed once with/bin/true; teardown uses its own context so a cancelled turn still cleans up. State in~/.blitz/sandboxes. - SK-51
os: Seatbelt/bubblewrap built per request — writes only to writable paths and a private temp, blocked paths hidden, network as allowed; rest of the filesystem readable. - SK-52
auto(default): gVisor if its test run works, else the OS sandbox (the reason is shown bydoctor). There is no unsandboxed fallback: without a sandbox (e.g. Windows) scripts don’t run (ErrNoScriptBox).
- SK-60
/envslists key, packages, skills that used it, size, last use, ready;/envs remove <key>;/envs pruneremoves incomplete environments and those no policy-allowed script needs.
CI’s Linux build runs the tests that install for real, which need the network (BLITZ_PYENV_TESTS=1, BLITZ_UV_PYTHON), with Node 22 and uv set up, and fails if any skips (BL-SK-06).
- SK-70
/skills list|show <name>|search <q>(andinstall,registry: SK-92, SK-96);showgives scripts, dependencies, tier, what the policy allows, and the content hash (fortrusted_hashes).doctorreports unparseable skills, blocked scripts and the script sandbox.
2026-10-14 (#36). Skills installed from registries such as Castor (pkg/skillreg). A registry is an index anyone can serve; Castor serves it from its registered, hashed skills (its CompiledReference hash is an index entry’s sha256).
- SK-90 The index (version 1, JSON):
{"version": 1, "name": "...", "skills": [{"name", "description", "version", "archive_url", "sha256", "castor_id?"}]}.archive_urlis absolute or relative to the index;sha256is the archive’s hash, hex. Another version is refused, naming it. - SK-91 Registries are an https URL (http only for localhost) or a local file, added with
blitz skills registry add <url> [--name n] [--token-stdin]or Settings › Skills, checked by reading the index first, named after it unless named. They’re kept in~/.blitz/skill-registries.toml(0600), a token in the system keychain (skill-registry:<name>), never in a file. The token goes asAuthorization: Bearerto the registry’s own host only, never to an archive elsewhere; errors name URLs without their query. Removing a registry forgets its token; its skills stay. - SK-92 Install (
blitz skills install <registry>/<name>|<name> [--project],/skills install … [--project], Install in Settings): the archive (a .zip, at most 32 MB) must matchsha256; it may hold at most 1,000 regular files and 64 MB unpacked, nothing outside it, no links;SKILL.mdat its root or in its one folder must name the skill. It’s unpacked beside the skills and swapped in, into~/.blitz/skills/<name>or<workspace>/.agents/skills/<name>, and recorded with its registry, version and hash. A folder of that name Blitz didn’t install is never replaced (ErrExists); a name two registries offer must be qualified.skills.policyapplies when it loads, as to any skill. - SK-93 Update (
blitz skills update [name…], Update all) reinstalls those whose registry now gives another hash, in place; uninstall removes only a skill it installed; list shows them. - SK-94 Search (
blitz skills search [words];/skills searchadds “In your registries” when there are any; Settings) matches every word in the name or description across registries; one that can’t be read is reported while the others answer. - SK-95 Live: the skills provider remembers a stamp of each folder and searches them again when one changes, so a skill installed, updated, edited or removed (from a registry, by
/skills update, by the agent, in an editor) is seen by open sessions at its next look, without a restart. 2026-10-16: the stamp is everySKILL.md’s path, size and modification time and its folder’s time (it was the top folder’s time alone, so aSKILL.mdedited in place was missed until a restart); the look skips.git,node_modules,.venv,venvand__pycache__, which a skill may keep an environment in. Each turn looks (Provider.Refresh), and a change rebuilds the agent, whose prompt lists the skills;/reloadsearches again regardless (Provider.Reload) (spec_memory_012 MEM-25). - SK-96 The desktop app: Settings › Skills (advanced) lists the registries (with a key when one has a token; remove), adds one (index address and an optional token), finds skills and installs them for the user or the active workspace, and lists those installed with Update all and uninstall, through
ConfigService(ListSkillRegistries,AddSkillRegistry,RemoveSkillRegistry,SearchRegistrySkills,InstallRegistrySkill,UpdateRegistrySkills,UninstallRegistrySkill; reasonsNO_REGISTRY,NOT_OFFERED,AMBIGUOUS,HASH_MISMATCH,SKILL_EXISTS,NOT_INSTALLED,REGISTRY_FAILED)./skills registryopens it. - SK-96a Since v0.7.1 (the owner’s request). Settings is global, so Settings › Skills holds no workspace’s skills: the registries come first (SK-96), then Built in, the skills Blitz ships with, by name, with their version and description (
ConfigService.ListBuiltinSkills, fromskills.Builtins, no workspace needed). A workspace’s own skills are that workspace’s (next).
- SK-97 A skill is one of four kinds, told from its folder rather than a field (
Skill.Kind): simple (SKILL.md alone), references (areferences/folder), scripts (scripts, declared or found; it may have references too) and meta (front matterskills:names other skills).SkillInfo.kindsays which. - SK-98 References and found scripts (
scanBundle, for skills on disk and built in): every file underreferences/is listed asreferences/<path>(Skill.References, also inResources), returned byactivate_skill(references) and read with the new toolread_skill_file(skill_name, path), confined to the skill’s folder (64 KiB, then truncated). When the front matter declares no scripts, each.pyand.tsinscripts/is a script named by its file (ScriptDefinition.Found,SkillScript.found), under the same policy, approval tiers and sandbox as a declared one. A skill that declares its scripts keeps exactly those: its other files there may be helpers they import. - SK-99 Meta skills and agents: front matter
skills: [a, b]makes a skill combine others;activate_skillreturns each member (members: name, instructions, references, scripts, error), theirs in turn, once each, so a loop ends (at most 16). Front matteragents: [name]offers a skill only to those agents: it’s left out of the system prompt’s skill list for another agent,list_or_search_skillsdoesn’t list it, andactivate_skillandread_skill_filerefuse it; none means every agent. Both fields are written so other tools ignore them. - SK-100 Studio › Skills (
SkillsPane, spec_studio_040): the workspace’s own skills (a folder inside the workspace:SkillInfo.project) fill the pane with their kind; a skill’s form slides in over the list (the agent form’s pattern) with tabs General, References and Scripts, and Back, Cancel and Save (asking first with unsaved edits, in the form or an open file). General: what it’s for (the name follows it until typed), name (lowercase letters, digits and single hyphens, up to 64:skills.ValidName), instructions with Draft (DraftManifestkindskill) and{secrets:completion, Combines (members, chosen from every skill the workspace sees), Offered to (the workspace’s agents) and the tools (allowed-tools, the agent form’s picker).WorkspaceService.GetSkillFilereturns a skill with SKILL.md’s hash and whether it’s editable (the workspace’s own);SaveSkillwrites.agents/skills/<name>/SKILL.md(or the existing skill’s folder, renamed when the name changes) if the hash still matches (HASH_MISMATCH), keeping every front matter field the form doesn’t edit, in order (skills.Render); problems (a bad or taken name, a built-in’s name, no description, a member that’s itself) come back with nothing written.DeleteSkillremoves its folder. - SK-101 References tab, a list and a file as Studio’s lists and forms are: the files in
references/fill the tab (an icon by kind), with New file (a name, then Create) and Upload… (several at once); opening one gives it the whole tab, with a back arrow and the skill’s buttons stepping aside. A text file opens in a small editor (CodeArea: highlighting,{secrets:completion, ⌘S) and is saved on its own (Save file, Revert); Markdown, SVG and tables switch between Source and Preview; a PDF, an image, audio or video shows as the Files editor shows it (Preview, through the file’s path in the workspace), with Replace…. Delete is asked first.ReadSkillFilereads any skill’s file;WriteSkillFileandDeleteSkillFileonly a workspace skill’s, underreferences/orscripts/, throughos.Root(INVALID_PATHotherwise). - SK-102 Scripts tab, the same list and file: each script with its language, where it comes from (found in
scripts/, or declared) and the policy’s verdict (allowed, or why not); New script from a template (Python, TypeScript, JavaScript, shell, cmd or PowerShell); the open script takes the tab’s width and height, with Redraft (kindscript, with its language) and, folded under it, Test run: arguments (split as a shell would split words and quotes), thenRunSkillScriptruns it asrun_skill_scriptwould (policy, sandbox, deny rules), its approvals given since the user starts it, and shows the exit code, output and errors. - SK-103 Add from…: every skill the workspace sees (built in, the user’s, a plugin’s or registry’s, the workspace’s), searched by name and description, each with Copy (
CopySkill: its files into.agents/skills/<name>under a name of its own, front matter kept but the name) and, with a skill’s form open, Reference (it becomes one of that skill’s members). - SK-105 Shell and cmd scripts: language
shell(alsosh,bash;.shinscripts/) runs with/bin/sh, andcmd(alsobat,batch;.cmd,.bat) withcmd.exe /d /c(%ComSpec%), in the same sandbox with the same tiers, timeouts and environment (SKILL_DIR,SKILL_OUTPUT); neither takes packages or an entry point. The policy blocks a shell script on Windows and a cmd script anywhere else, saying why (platformReason), andskills.policy.languagesallowsshellandcmdby default. - SK-106 JavaScript and PowerShell: language
javascript(alsojs,node;.js,.mjs) runs with node exactly as TypeScript does (npm packages, entry point), andpowershell(alsopwsh,ps1;.ps1) withpwsh -NoProfile -NonInteractive -ExecutionPolicy Bypass -File, else Windows PowerShell, failing with why when neither is installed. Both are inskills.policy.languagesby default (config.ScriptLanguages). Scripts found inscripts/that share a name (test.py,test.ts) are each named by their whole file name, so running one never runs the other. - SK-107 Packages from the folder: a Python script that names no dependencies takes those of the first Python package file found,
requirements.txt(a line each, comments and blank lines dropped), thenpyproject.toml([project] dependencies), each inscripts/before beside SKILL.md; a JavaScript or TypeScript one takespackage.json’sdependencies(name@version).SkillInfo.package_fileslists the ones the folder has. The Scripts tab’s Packages section lists them (which one counts, and which is unused because another of its kind comes first), opens each in the editor, and offers Add requirements.txt and Add package.json (inscripts/, from a template) when there’s none of that kind; each script’s row says how many packages it gets. Studio may write these three files beside SKILL.md as well as inscripts/. They’re checked as declared ones are (plain requirements only, never URLs, paths or pip options;skills.policy.packagesallow and deny; pins when required), installed into an isolated environment kept by Blitz for exactly that list (not in the skill’s folder, so it isn’t committed or changed by the script), wheels only, after an approval that can be remembered for that list, and the script runs with it read-only in the sandbox. Apackage.jsonthat isn’t JSON, or apyproject.tomlthat isn’t TOML, is a refused dependency saying so. - SK-108 Reviewing scripts: a skill on disk with scripts (not a built-in) runs them unasked only once the user has reviewed and trusted its content:
config.SkillTrust(skill-trust.jsonin the settings directory) keeps each skill folder’s trusted content hash, read afresh at each check. Until then (a new or edited script, a package file changed, a registry install or update)skills.EvaluatesaysNeedsReview(not a block:SkillInfo.needs_review), andrun_skill_scriptasks before each run whatever the tier, an ask rule’s way (MustAsk: an unattended run, a worker’s, is refused). Trusting the project’s settings, whose hash includes its skills’ content, trusts those skills as they are (at open, and onTrustProject), so a trusted workspace isn’t asked twice.ConfigService.GetSkillReview(dir)says what the scripts are (name, language, file, packages) when they need review, andTrustSkillScripts(dir, hash)records the content shown (HASH_MISMATCHif it changed since;SKILL_NOT_FOUND). The desktop app asks: after a registry install with scripts (Settings › Skills; the installed list’s Review scripts while one waits), and in Studio, whose list marks a skill Scripts to review and whose skill view has a banner with Review scripts; the review dialog lists the scripts and offers Trust these scripts or Not now. Studio’s Test run, which the user starts, isn’t asked about. While a script runs, the chat’s tool row says Running… with a spinner (every running tool has the spinner), and Test run’s button spins. - SK-104
\skill-namein a prompt (where a word starts: the start, after whitespace or an opening bracket or quote) sends that skill’s instructions with the prompt, as/skill-namedoes, with its references and a meta skill’s members to activate; once each, only for skills offered to the active agent. The transcript keeps the prompt as typed. The composer completes it as it’s typed (skillMention.ts: names starting with what’s typed first, then names and descriptions containing it; the list asked for every 30 s, and again after Studio changes a skill).
A network field in Castor’s proto.