Skill library sources
An open-source skill library is added by a reviewed declaration in skill-sources.yaml, not by changing importer code. The file only lists allowed sources. Nothing reads it during a run. Only scf skills import, scf skills evaluate and scf skills update fetch from the network, and never during a run.
schema: 1
sources:
- id: example-skills
repository: https://github.com/example/skills
status: enabled
ref:
approved: 0123456789abcdef0123456789abcdef01234567 # full commit id
track: main # only to look for updates
paths: [skills]
adapter: agent-skills
trust: community
licence: { allow: [MIT, Apache-2.0] }
update: { cadence: weekly }
owner: "@octocat"
review:
by: "@octocat"
on: 2026-10-13
commit: 0123456789abcdef0123456789abcdef01234567 # same as ref.approved
licence: MIT
Fields.
id(lower-case slug, unique),repository(anhttps://git address),status,reason,ref,paths(1 to 20 plain folders;.is the root; no globs; no folder inside another),adapter(agent-skills),trust(first-party,verifiedorcommunity),licence.allow(accepted: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD, CC0-1.0, CC-BY-4.0, Unlicense),licence.reviewed(optional; the licence review digest, see Licence review and provider assumptions),update.cadence(manual,daily,weeklyormonthly),update.reviewer(optional; a GitHub user or team, asowner; see below),owner(a GitHub user or team such as@octocator@acme/skills) andreview(see below). Unknown keys are refused.Refs.
ref.approvedis the release and must be a full commit id (40 or 64 hex digits). A branch or tag can move, so it is only allowed inref.track, which names what to watch for updates. A cadence other thanmanualneedsref.track.Status.
enabled,disabled,quarantinedorretired. Onlyenabledsources may be used. An enabled source needsref.approvedand has noreason; every other status needs areason.Review of a community source.
reviewrecords who checked the maintainer and the licence, and when. It has four fields:by(a GitHub user or team, asowner),on(YYYY-MM-DD, not in the future),commit(the full commit id, equal toref.approved) andlicence(one oflicence.allow). An enabledtrust: communitysource is refused withoutreview, and whenreview.commitis notref.approved. A new commit needs a new review, so changing one word cannot raise the trust.Known community repositories. The Foundry ships a list of community repositories (
src/skills/community-rules.ts).github.com/jiangxianze/spring-agent-skillsis on it, so a source with that repository must saytrust: community; any other trust is refused and the message namestrust.Source profile. An optional
profilesays which upstream capabilities are portable and which belong to a vendor. It is for the maintainer of the catalogue:profile: provider: confluent # a slug; required when profile is set portable: # upstream capability -> catalogue capability (at most 64) streams: kafka-streams products: # default provider products with version ranges (1 to 16) kafka: ">=3.7"Every other upstream capability becomes
confluent-<capability>, so a vendor skill cannot take over a portable capability by accident. A skill with a vendor capability gets the provider block. A source withoutprofileis unchanged.scf skills sources checkprintsprovider confluent (1 portable)for the source. It refuses a mapping to an invalid slug, a duplicate key and aprofilewithoutprovider. A slug longer than 64 characters, or two upstream names that land on the same slug, is a problem that names the skill and the capability; nothing is dropped silently. The profile is not applied during import or scan yet.Owner and policy. Every source has an owner and an update cadence.
Reviewer for unattended updates.
update.reviewernames the GitHub user or team that reviews a new commit of an enabledtrust: communitysource whenscf skills update --dueruns without a person. The tool never invents a reviewer. Without it, such a source printsPROBLEM … needs update.reviewerand no pull request is opened. The field is part of this file, so it is set by a reviewed pull request.scf skills sources checkvalidates it; an existing file without it stays valid.Add a source by pull request. Add an entry, run
scf skills sources check, and let the owner and a reviewer approve the change.Connectors. A connector is a reviewed definition of one repository. An entry whose repository is a connector's repository must follow the connector's rules, whatever its
idis and however the address is spelled.scf skills connectorslists them offline; it reads no files. The connectors areawesome-copilot(https://github.com/github/awesome-copilot):pathsmust beskillsor a folder below it,adapter: agent-skills,trust: community, andlicence.allowonlyMIT; andanthropic-skills(https://github.com/anthropics/skills):pathsmust beskillsor a folder below it,adapter: agent-skills,trust: verified, andlicence.allowonlyApache-2.0, with a matchinglicence.reviewed(see Licence review and provider assumptions); andvercel-agent-skills(https://github.com/vercel-labs/agent-skills):pathsmust beskillsoragent-skillsor a folder below one of them,adapter: agent-skills,trust: verified, andlicence.allowonlyMIT(see Vercel agent skills). Any other value is aPROBLEMand exits 1. A connector ships without a commit, soref.approvedis still needed on each enabled entry.Check.
scf skills sources check [<file>]readsskill-sources.yamlin the current folder, or the file you name. It works offline and does not need the data folder. It lists each source and thenOK <n> sources, <m> enabled, and exits 0. A reviewed source is listed withreviewed by <by> on <on>. Otherwise it prints onePROBLEM <where>: <message>line per error and exits 1. Credential-like values are never printed.Import and scan.
scf skills importandscf skills scanread the same file. A community source without a currentreviewis refused as an invalid source list: aPROBLEMline names the missing or stale review, and nothing is fetched or read.
A community source that is not yet reviewed is kept disabled. This entry goes under sources:. It is an example for github.com/jiangxianze/spring-agent-skills; the folder and licence are to be confirmed at review:
- id: spring-agent-skills
repository: https://github.com/jiangxianze/spring-agent-skills
status: disabled
reason: not reviewed yet
paths: [.]
adapter: agent-skills
trust: community
licence: { allow: [MIT, Apache-2.0] }
update: { cadence: manual }
owner: "@octocat"
Imported library content is kept in <data folder>/skill-quarantine/. This folder is never scanned for skills, also when it is listed in skills.roots, reached through a link, or allow_unreleased is on. Nothing in it is run.
Source profiles#
skill-source-profiles.yaml at the repository root says what each selected skill of a source provides and needs. It sits next to skill-sources.yaml, and the same source id is used in both. The shipped file has sources: [], which is valid. A listed source needs at least one skill. Nothing reads the file during an import or a run yet.
schema: 1
exclusive: [cosmos-db-access]
sources:
- id: example-skills
skills:
- name: cosmos-db-setup
class: vendor
capabilities: [cosmos-db-access]
high_risk: [cosmos-db-access]
requires:
config: [COSMOS_ENDPOINT]
tools: [network]
connectors: [azure-cli]
live_services: [COSMOS_KEY]
evaluations: [smoke-test]
- Fields.
id(a lower-case slug, unique, the same as inskill-sources.yaml) andskills(1 to 100 entries). Each skill has aname(a slug, unique in its source),class(portableorvendor),capabilities(1 to 32 slugs),high_risk(0 to 32 capabilities, each one of the skill'scapabilities),requiresandevaluations(0 to 16 slugs).exclusive(0 to 200 slugs) lists the capabilities that allow only one winner. Unknown keys are refused. requires.configandlive_serviceshold names only: a slug or an environment variable name such asDATABASE_URL, never a value.toolsis one or more ofshell,networkandwrite.connectorsis a list of slugs.- Rules.
high_riskis a subset ofcapabilities. A capability belongs to eithervendororportableskills, not both, so use separate capability ids for each class. - Check.
scf skills profiles check [<file>]readsskill-source-profiles.yamlin the current folder, or the file you name. It works offline and does not need the data folder. It lists each skill with its class, capabilities, high-risk capabilities, requirements and evaluations, thenOK <n> skills in <m> sources, <k> exclusive, and exits 0. Otherwise it prints onePROBLEM <where>: <message>line per error and exits 1. Credential-like values and URLs with user info are never printed.
Evaluate the profile gates#
scf skills profiles evaluate <source-id> shows, for each skill of a staged import, whether it passes the gates it must pass before promotion. It is offline and read-only: nothing is written, promoted or locked, and no credential or project value is read. The command is profiles evaluate; scf skills evaluate is the source report of Evaluate a source.
scf skills profiles evaluate example-skills
PASS cosmos-db-setup (cosmos-db-setup) vendor capabilities=cosmos-db-access high_risk=cosmos-db-access sha256:…
NEEDS cosmos-db-setup config=COSMOS_ENDPOINT live_services=COSMOS_KEY
BLOCKED sql-migrations (sql-migrations) portable capabilities=sql-migrations high_risk=- sha256:…: evaluation: correctness not recorded
MISSING example-skills/backup-tuning: in skill-source-profiles.yaml, not staged
EVALUATED 2 skills from acme/skills@3c1d… sha256:…: 1 pass, 1 blocked, 1 missing
- Gates. Each blocked skill lists its reasons, by gate, in this order:
declared: the staged skill has no entry inskill-source-profiles.yaml.licence: the stored licence evidence must beallowed. Without evidence the reason says to run the import again.security: the skill must normalize and have no open high finding. This is the same decision asscf skills scan.requirements: every tool or connector the package needs must be in the skill'srequires. An undeclared need blocks.evaluation: each kind in the skill'sevaluationsneeds a passed record, see below.
- NEEDS. The configuration names and live services a profiled skill declares, as names only. The command grants nothing with them.
- MISSING. A profile entry with no staged skill. It is reported, and it is not a block.
- Each line shows the id, the class, the capabilities, the high-risk capabilities and the digest. A blocked line shows the digest too, so you can copy the id and digest into a record.
- Exit code. 1 when a skill is blocked, a
PROBLEMline is printed or the canonical recommendation has aCONFLICT(see Canonical skill per capability); otherwise 0.
Evaluation records#
skill-profile-evaluations.yaml in the current folder holds the owner's results. It is changed by pull request and read only by profiles evaluate and, for a trust: community source, by Compare an import (the kind: fixtures records). An absent file means no records, and every required evaluation is then not recorded.
schema: 1
evaluations:
- skill: sql-migrations
digest: sha256:9d1e0b7a…
kind: correctness
verdict: passed
owner: "@acme/skills"
reviewed: 2026-10-01
expires: 2027-04-01
report: 7c3f…
- Fields.
skillandkindare slugs,digestis the package digest,verdictispassedorfailed, andowneris a GitHub user or team.reviewedandexpiresare dates.reportis optional and may hold the digest of a scored record. Up to 500 records. - Lifetime.
expiresis at most 365 days afterreviewed, and not before it.reviewedmay not be in the future. - Matching. A record counts only for its skill id, its kind and its exact digest. A new upstream digest voids it. A
failedrecord at that digest blocks even when apassedone exists. An expired record blocks. - Refused. Wildcards, unknown keys, credential-like text and control characters. An invalid file prints
PROBLEM skill-profile-evaluations.yaml <where>: <message>lines, and nothing is evaluated.
Canonical skill per capability#
When an exclusive capability of a profiled source is provided by two or more skills, scf skills profiles evaluate <source-id> also recommends one of them. The lines come after the per-skill lines and before EVALUATED. The same rules as Overlap with the catalogue apply: a canonical skill is chosen by the same levels, and the rejection codes are the same.
- Which capabilities. Only an
exclusivecapability that a skill of this source declares inskill-source-profiles.yaml. The providers are the staged skills of the source and the active skills registered locally (built-in and administrator). Installed copies from the repository are not compared. - Blocked candidates. A staged skill that fails a gate of the evaluation above is never recommended. Its failed gates are the evidence:
gates=lists them by name (declared,licence,security,requirements,evaluation). A staged skill whose required evaluation is not recorded is rejected asdiscovery-only. A staged skill with no profile entry is not a provider at all. - Installed copies. A registered skill that is the same artifact as a staged one and declares the same capabilities keeps its own place and origin. The staged gate failures and the evaluation result are added to it.
- High risk. A capability listed in a profile skill's
high_riskis markedhigh-riskon its lines. - Lines.
CANONICAL <capability>: <id@version> <origin> (<level>)names the winner, its origin (staged,builtinoradmin) and the level that decided it.only-candidatemeans nothing else competed.CONFLICT <capability>: <rule>: <message>means the choice cannot be decided, for exampletie high-riskwhen two skills tie on a high-risk capability.NONE <capability>: no skill can be usedmeans every provider is rejected.REJECTED <capability> <id@version> <origin>: <code>gives each loser its code, then(<level>)when a level decided it, thengates=…when the candidate failed gates.- Only validated ids, versions, codes and the selector's own message are printed. No name, description or path of an upstream skill is printed.
- Exit code. 1 for a
CONFLICT, a blocked skill or aPROBLEMline. ACANONICAL,NONEorREJECTEDline alone does not change it. - Catalogue problems.
PROBLEM catalogue <source> <kind>means the registered skills could not be read in full. It exits 1. - Limits. The staged and the registered skills together are at most 1,000. A larger set prints
PROBLEM too many skills or capabilities to compareand exits 1. - Read only. The recommendation grants nothing: nothing is promoted, pinned or loaded, and no run reads it. Runs choose skills as before.
For the fixture of the tests, the first output has no correctness record for the portable skill. The second output has a passed database-correctness record for it (ids and versions are examples):
CANONICAL postgresql: postgres-basics@1.0.0 staged (only-candidate)
REJECTED postgresql postgres-practices@1.0.0 staged: discovery-only gates=evaluation
CANONICAL postgresql: postgres-practices@1.0.0 staged (evaluation)
REJECTED postgresql postgres-basics@1.0.0 staged: outranked (evaluation)
Example: Supabase agent skills#
This example adds the Supabase skills for PostgreSQL, authentication, row-level security and backend patterns, from https://github.com/supabase/agent-skills. It is for the owner of the catalogue; nothing in the repository holds the commit, the project or a key. The folder names and the licence are placeholders. They could not be checked against the upstream repository (no network), so check them against the commit you review before you use the entries. Each # placeholder comment marks a value to replace.
The entry in skill-sources.yaml, under sources::
- id: supabase-agent-skills
repository: https://github.com/supabase/agent-skills
status: enabled
ref:
approved: 0000000000000000000000000000000000000000 # placeholder: the reviewed full commit id goes here
track: main # only to look for updates
paths: [skills] # placeholder: check the folder names
adapter: agent-skills
trust: verified # the vendor's own repository
licence: { allow: [MIT] } # placeholder: check against the approved commit
update: { cadence: manual }
owner: "@octocat"
The matching entry in skill-source-profiles.yaml, with exclusive: [postgresql] at the top of the file:
schema: 1
exclusive: [postgresql]
sources:
- id: supabase-agent-skills
skills:
- name: supabase-postgres-best-practices # placeholder: check the folder name
class: portable
capabilities: [postgresql, postgresql-query-performance, postgresql-schema-design, postgresql-row-level-security]
high_risk: [postgresql-row-level-security]
evaluations: [database-correctness]
- name: supabase # placeholder: check the folder name
class: vendor
capabilities: [supabase-auth, supabase-rls-policies, supabase-edge-functions, supabase-realtime]
high_risk: [supabase-auth, supabase-rls-policies]
requires:
config: [SUPABASE_URL, SUPABASE_ANON_KEY]
tools: [network]
live_services: [supabase]
- Approved commit.
ref.approvedis a placeholder. The owner replaces it with the full commit id that was reviewed, in a pull request. Until then the source cannot be imported. - Portable and vendor capabilities. The portable skill provides the four PostgreSQL capabilities. The vendor skill provides the four Supabase capabilities. Each capability belongs to one class only, so a vendor skill cannot take over
postgresql. - Exclusive. Only
postgresqlis exclusive. When the portable skill and another skill provide it,scf skills profiles evaluate supabase-agent-skillsrecommends one of them. The Supabase capabilities are not exclusive and get noCANONICALline. - High risk. The auth capability (
supabase-auth) and the row-level-security capabilities (supabase-rls-policiesandpostgresql-row-level-security) are inhigh_risk, so they are markedhigh-risk. - Evaluation. The portable skill needs a passed
database-correctnessrecord for its exact digest inskill-profile-evaluations.yaml. Without it the skill is blocked and cannot be recommended. The vendor skill needs no record here. - Configuration names only. The project URL and the anon key are read as the names
SUPABASE_URLandSUPABASE_ANON_KEY. Their values are never stored in the repository, printed or sent to a command. - Order of checks. Run
scf skills sources check,scf skills profiles check,scf skills import supabase-agent-skills,scf skills scan supabase-agent-skillsand thenscf skills profiles evaluate supabase-agent-skills. Each one is read-only, except the import, which only stages the skills in quarantine.
Owner overrides and project pins#
Two reviewed records can choose one approved skill for a capability that allows only one. Both are checked with the CLI; no run reads them yet, so runs choose as before.
- Project pin (
.claude-factory/skill-pins.yaml): read from the default branch of the repository, never from a work branch. One pin per capability. - Owner override (
skill-overrides.yaml): for all repositories, or for a list of them (owner/name). A repository override beats analloverride for the same capability. A project pin beats both.
Each entry names the capability, the target (id@version), who chose it (by, a GitHub user or team), a reason, the start (from), and expires or review_by (or both). A target must be an approved, usable version that passes the licence, safety, tools and compatibility checks. Remove the entry to return to the default choice. scf skills overrides check and scf skills project-pins check print each entry with its digest and exit 1 for an invalid or expired entry.
Import a source into quarantine#
scf skills import <source-id> [<file>] reads skill-sources.yaml (or the file you name), fetches the approved commit of an enabled source and stages its skills in quarantine. Nothing is loaded or run.
scf skills import example-skills
REJECTED skills/link: a symbolic link
LICENCE FILES 2 staged, 0 not readable
OK 2 skills staged from github.com/example/skills@0123456789ab sha256:… (staged)
Quarantine: <data folder>/skill-quarantine/example-skills/0123456789abcdef0123456789abcdef01234567
- Layout.
skill-quarantine/<source-id>/<commit>/import.jsonandfiles/<upstream path>. The licence and notice files of the same commit are kept with them:licence.jsonandlicence/<upstream path>. import.json.schema,source,repository,repositoryKey,commit,paths,adapter,digest,skills[]andrejected[]. An import also hasretrievedAt(the UTC time of the fetch) andlicenceFiles[]: each file'spathwithsha256andbytes, or with aproblemwhen it cannot be read or staged. The list is the same as inlicence.json, and the two are checked against each other when the import is read. An import written before these fields existed has neither; it still reads, and its licence evidence is empty.- Licence and notice files. Every
LICENSE,NOTICEand similar file of the staged skill roots is copied byte for byte intolicence/, with its path, size and SHA-256 inlicence.json. The copy is checked against that record when it is read. Limits: 200 files, 256 KiB per file and 4 MiB in total; more than that stages nothing and prints aPROBLEM. - An import from before licence files were kept. The first import of that commit after the upgrade replaces its folder. The old folder is kept as
.old-<commit>until the new one is verified, and the next import removes it. Until then a reader of the old folder reports that there are no licence files and asks you to import again.scf skills scanworks on it as before. - Licence files. They are untrusted bytes. The import never prints their content and never follows a link in them.
- How it fetches. One commit, into a bare temporary repository. There is no checkout. Only https is used, with no redirects. Your git configuration, credential helper and
~/.netrcare not used. The temporary folder is removed at the end. - Limits. 120 s for the fetch, 256 MiB during the fetch, 8 MiB for the file list, and the per-skill limits from the staging step (#457).
- Rejections. A skill is rejected, and not staged, for example when it is a symbolic link, a submodule or too large (
is larger than 262144 bytes), or has an unsafe path. Each prints aREJECTEDline with the reasons. The other skills are still staged. (staged)and(unchanged). A second import of the same commit prints the same digest and(unchanged). If the folder holds a different import of this commit, the result isconflict: remove the folder and import again.- Ctrl-C. The import stops and nothing is staged. Once writing into the quarantine has started, the import is finished.
- Exit codes. 0 when all skills are staged. 1 when a skill is rejected or a
PROBLEMis printed. Usage errors exit 1 with the usage text. - Known limits. The memory git uses while indexing the pack is not capped. A renamed upstream repository fails until the manifest is updated. Private sources are not supported.
Scan an import#
scf skills scan <source-id> [<exceptions-file>] prints the security report of the staged import of a source. It is offline and read-only: nothing is written, loaded or run, and no process is started. It reads skill-sources.yaml in the current folder for the approved commit (ref.approved), then the staged import in quarantine.
scf skills scan example-skills
FINDING <16 hex digits> high open <skill> <path>[:<line>]: <evidence>
BLOCKED <skill>: 1 high finding
ADDRESS <skill> mentioned references/pom.xml: https://repo.maven.apache.org/maven2 (under references/, which the agent only reads)
EXCLUDED <skill>: needs network access
SCANNED 2 skills from github.com/example/skills@0123456789ab sha256:…: 2 blocked
- What it checks first. The staged files must match
import.jsonexactly: a changed file, a link or an extra file is refused with aPROBLEMline and nothing is scanned. Then every skill is scanned with the static scanner (#472), and the exceptions are applied. - Lines. One
FINDINGline per finding: its stable id, severity, disposition (open,accepted,expiredorreview-required), the skill, the path (with:<line>when the line is known) and a short bounded evidence text. A credential is never printed; it shows as(hidden). Then one line per skill:OK <skill>: <n> findings, none blocks, orBLOCKED <skill>: <n> high findings. EachREJECTEDline of the import is repeated, with credential-like text shown as(hidden). - Not normalized. A skill that the normalization step rejects (for example a credential or bad metadata) has no package id. Its findings are still printed, with the folder as its name, and it prints
BLOCKED <folder>: not normalized (<reasons>). - What blocks. A high finding that is not accepted blocks its skill. Medium and low findings never block. An accepted finding does not block; a secret finding can never be accepted.
- Quoted examples. A prompt-injection phrase in quotation marks is a
lowfindingprompt-injection.quoted-exampleand never blocks, when a word such as "e.g.", "such as", "ignore", "refuse", "detect", "watch for" or "treat as data" introduces it in the same sentence. The word may be on the same line or the line before. There must be no "not" or "never" before it, and no instruction such as "follow", "execute" or "say" in the sentence before the quote (unless it is negated, as in "do not follow"). TheFINDINGline gives the path and line. Anything else (an instruction, no quotes, no cue, a cue in another sentence) stayshigh. Credential, policy and network rules have no such exception. - Web addresses. One
ADDRESS <skill> <mentioned|called> <path>: <address> (<why>)line per web address (http://orhttps://) in the instructions and in the files of the skill.mentionedmeans the agent only reads the file: Markdown, data,metadata.json, anything underreferences/orevals/.calledmeans the file is code: anything underscripts/, a file with a code extension (.sh,.py,.js,.ts,.html,.ps1and similar), a file without an extension, or a file that starts with#!. Amentionedaddress never blocks. Acalledaddress blocks the skill asholds a web address in code. Addresses are shown with the user name, password and credential-like query values hidden ((hidden)), and cut at 80 characters followed by a short hash, so two long addresses stay apart.scf skills promote checkprints the same lines. - Excluded skills. A skill that needs a live integration (a connector or MCP tool), needs network access, uses a network command, makes a web call from code, holds a web address in code (see above), ships plugin or MCP configuration (
.mcp.json,plugin.json, a.plugin/folder) or declaresmcp_toolsprintsEXCLUDED <skill>: <reasons>instead ofOK. It counts as blocked. Exclusions are not lifted by the exception file. - Credential files in shell tests. A shell file test such as
[[ -f .env ]],[ -d .ssh ]ortest -f .envis not an upload and raises no finding. A real upload of a credential-like file is stillhigh:curl -F "f=@.env",curl -T .env,curl --data-binary @.env,scp .env …ornc … < .env. - Exit codes. 0 when no skill is blocked. 1 when a skill is blocked or excluded, or a
PROBLEMis printed. APROBLEMnames the file or field and never shows a credential-like value. - Problems. A missing import (
no staged import … run "scf skills import"), an unknown source, a source withoutref.approved, an invalid source list, an invalid exception file, or a named exception file that does not exist. A missing default exception file means "no exceptions". - Connector report. A source with a reviewed connector profile (today:
github.com/prisma/skills) also gets one line per mapped skill:CONNECTOR <connector> <skill>: specific=… general=… orm=… engines=… runtime=… needs=… risk=… evaluations=… reviewed=yes|no.CONNECTOR … unmappednames a skill the profile does not know or whose companion skill is missing;CONNECTOR … absentnames a profile skill that is not in the import. The report reads nothing else and runs nothing.reviewed=nomeans the skill's commit is not one the profile was checked against, so the skill fails the safety gate and is not offered.
The exception file#
skill-scan-exceptions.yaml next to skill-sources.yaml lists the findings the owner has reviewed. It is changed by pull request and read only by this command. Each entry accepts exactly one finding of one skill version:
schema: 1
exceptions:
- finding: 3f2a9c1d7e8b4a60 # the id from the FINDING line
skill: example-demo # the skill as the FINDING line names it
digest: sha256:… # the skill's digest in import.json; a new version has a new digest
reason: "Reviewed: the script only prints a version"
owner: "@octocat"
reviewed: 2026-10-13 # YYYY-MM-DD, not in the future
expires: 2027-01-13 # at most 365 days after reviewed
- Expiry. Use
expires(a date), orreview: requiredwhen the finding must be looked at again. An entry whose date has passed isexpired, andreview: requiredisreview-required; neither accepts the finding, so the skill stays blocked. - Limits. At most 200 entries. No wildcards. Unknown keys are refused. Every problem names its entry and field.
Assess an import#
scf skills assess <source-id> prints the workflow report of the staged import of a source: which of its skills fit the Foundry's flows, and which do not and why. It is offline and read-only, like scan: nothing is written, loaded or run, and it needs no data folder. It reads the approved commit of the source (ref.approved in skill-sources.yaml) and the staged import in quarantine. It takes no exception file.
scf skills assess example-skills
CONFLICT <code> <effect> <skill> <path>[:<line>]: <evidence>
CANDIDATE <skill> (<name>): tools <list>; roles <list>; flows <list>; context <tokens>/<max>
EXCLUDED <skill> (<name>): <code>, <code>
ASSESSED <n> skills from <repository>@<commit> sha256:…: <n> candidates, <n> excluded with profile <id>
- Profile. Only the sources with a workflow profile can be assessed; today that is
github.com/obra/superpowers. Another source printsPROBLEM no workflow profile for …and exits 1. The profile is insrc/skills/workflow-rules.tsand lists the individual skills that may become candidates, each with its tools, roles, capability, built-in flows and token budget. A skill that is not in the profile is excluded withnot-selected. - The declaration is the ceiling. The profile says what a skill may need. Text or metadata in the upstream skill that asks for more (tools, connectors,
allowed-tools, scripts without a shell, a dependency on another skill, a delivery step, a worktree or subagent assumption) excludes the skill. It never raises the declaration, and nothing is adapted or merged: the upstream text is not changed. - Flows. A declared flow counts only when one of its Claude steps really receives the skill (role coder, skills selected) and gives every declared tool. A flow with
skills: offor without such a step isflow-incompatible; a skill with no compatible flow isno-compatible-flow. - Lines.
CONFLICT <code> <effect> <skill> <path>[:<line>]: <evidence>for each finding. The effect isexclude(the skill is left out) orreport(it is shown and does not change the status). Evidence is short and bounded, and a credential-like value is shown as(hidden). At most 50 findings are shown per skill;CONFLICT omitted report <skill>: and <n> moresays how many were left out. The exclusion codes are counted before the cut, so a hidden finding still excludes. - Candidate or excluded.
CANDIDATE <id> (<name>): tools …; roles …; flows …; context <tokens>/<max>for a skill that passes.EXCLUDED <id> (<name>): <codes>lists every exclusion code, sorted. A skill that normalization rejects isEXCLUDED <folder>: not normalized (<reasons>). - Codes. Exclusions:
not-selected,assumes-subagents,assumes-worktrees,assumes-commands,own-delivery-flow,skips-approval,orchestration,grants-tools,undeclared-tool,requires-skill,context-over-budget,no-compatible-flow. Reports only:mentions-skill,flow-unknown,flow-incompatible,extra-files. - Exit codes. 0 when the report is printed, even when every skill is excluded: an exclusion is the expected result. 1 for a
PROBLEM(no staged import, unknown source, a source withoutref.approved, an invalid source list, or no workflow profile). - Not done. Nothing promotes a candidate yet.
workflowBlockers,declaredPackageandworkflowProvenanceinsrc/skills/workflow.tsare for a later promotion step to call. A promoted skill would record its declaration inSkillProvenance.workflow.
Compare an import#
scf skills compare <source-id> compares the staged import of a source with the built-in java and spring-boot skills. For each skill it shows the capabilities it shares with them, every collision, and one recommendation or conflict for each shared capability. It is offline and read-only, like assess: nothing is written, loaded or run, and it needs no data folder. It reads skill-sources.yaml and the approved commit (ref.approved) of the source, then the staged import in quarantine. For a trust: community source it also prints a promotion decision per candidate and reads skill-scan-exceptions.yaml and skill-profile-evaluations.yaml in the current folder (see Promotion decision below). Other sources take no exception file. The built-in skills are read straight from their folders; the lock file and the owner overrides are not used, except that skill-overrides.yaml in the current folder is read when it exists.
scf skills compare spring-agent-skills
SKILL <id>@<version> <tier> capabilities=<list or ->
EXCLUDED <id> <code>
NO-OVERLAP <id>
COLLISION <id> <class> <reason> <id@version,...> [blocking]
RECOMMEND <capability> <id@version> (<level>)
CONFLICT <capability> <reason>
NOTE <reason>
PROMOTABLE <id>
REFUSED <id>: <code>, <code>
REFUSED <folder>: not-normalized
COMPARED <n> skills from <repository>@<commit>: <n> collisions (<n> blocking), <n> conflicts
The PROMOTABLE and REFUSED lines are printed only for a trust: community source.
- Capabilities. Upstream tags, keywords and capability names are mapped to built-in capability names by a shipped table in
src/skills/community-rules.ts. For examplespring-bootwithtransactionsmaps tospring-boot-transactions, andjavamaps tojava-language. The mapping is used for this comparison only: the skill, its digest and its package are not changed. - Skills. One
SKILLline for each imported skill, sorted by id. Its tier isvetted-communitywhen the source is enabled and reviewed at the approved commit (review.commitequalsref.approved), anddiscovery-onlyotherwise. A skill with no mapped capability also printsNO-OVERLAP <id>. - Excluded.
EXCLUDED <id> stalemeans a declared version is no longer supported, andEXCLUDED <id> incompatibleorcompatibility-uncheckedmeans the skill cannot be checked against its declared versions. An excluded skill is never recommended. A skill that normalization rejects printsEXCLUDED <folder> not-normalized. - Collisions.
COLLISIONlists each collision with its class, its reason and the members it names.blockingat the end marks a collision that stops the comparison. - Recommendations. For each capability that an imported skill and a built-in skill share, exactly one line is printed.
RECOMMENDnames the winner and the level that decided it (for exampletrust-tier,owner-overrideoronly-candidate). The built-in skill wins on trust tier over avetted-communityskill, unless an owner override says otherwise.CONFLICTnames the capability and the reason, for example that no skill is eligible. - Notes.
NOTEgives the first reason that stops the canonical selection (the same text a run would stop with), and says how many more there are. It is printed only when there is one. - Order. The same input gives the same output in any order.
- Limits. The comparison is refused with a
PROBLEMwhen it would compare more than the allowed number of skills or share more capabilities than the selection rules allow. No project is given, so a skill is not checked against the versions of a project; only the stale check applies. - Promotion decision. For a
trust: communitysource, each staged skill gets one line after the report:PROMOTABLE <id>when every check passes, orREFUSED <id>: <codes>. The codes are listed in the order below. A staged entry that did not normalize printsREFUSED <folder>: not-normalized. These lines come beforeCOMPARED, which stays last.source-not-enabled,review-missing,review-stale,import-not-approved: the source is not enabled, has noreview, or its review or the staged commit does not matchref.approved.licence-missing,licence-ambiguous,licence-rejected,licence-review: the licence evidence of the import.licence-missingmeans the import has no licence evidence, so import it again. See Licence review and provider assumptions.excluded: the skill needs a live integration, network access or plugin configuration, as in Scan an import.import-mismatch: the skill's id is shared with another staged skill, or the skill does not belong to this source.scan-blocked: a high finding is not accepted inskill-scan-exceptions.yaml.compareruns the scan itself, so no scan record is needed.version-stale,version-unchecked: a declared version is no longer supported, or cannot be checked.collision-blocking,canonical-conflict: a blocking collision, or a shared capability with no winner.fixtures-missing: nokind: fixturesrecord for the skill at its exact digest, or the record has expired.fixtures-failed: the record at that digest isfailed. See Evaluation records.
- Inputs. For a
trust: communitysource, a missingskill-scan-exceptions.yamlmeans no exceptions, and a missingskill-profile-evaluations.yamlmeans no records. An invalid file printsPROBLEM <file> <where>: <message>, noCOMPAREDline, and exits 1. Fixture results come only from thekind: fixturesrecords, matched on the skill id and the exact digest. The gate records ofscf skills gateare not used, and a missing record is never read asfailed. - Exit codes. 0 when there is no conflict, no blocking collision and no
PROBLEM. 1 for aCONFLICT, a blocking collision, or aPROBLEM(no staged import, unknown source, a source withoutref.approved, an invalid source list, a built-in skill that cannot be read, an invalidskill-overrides.yaml, or an invalid exception or record file). For atrust: communitysource, 1 also when a candidate isREFUSED, or an entry did not normalize. Most community imports exit 1 until review, licence evidence and fixture records exist; this is intended, and scripts that usecomparewill see it. Other trust tiers keep the exit rule above, unchanged.
Evaluate a source#
scf skills evaluate <source-id> [<technology>@<version> ... | --repo <dir>] reports what an enabled source offers, before you import it. It fetches the approved commit (ref.approved) into a temporary folder, like scf skills import, and then writes nothing: no quarantine, no catalogue and no file.
scf skills evaluate oracle-skills oracle-database@19 oracle-apex@24
SKILL sql-tuning 1.2.0 skills/sql-tuning capabilities=database-sql risk=low
COMPATIBILITY sql-tuning: oracle-database >=19 (source); project: compatible
TOOLS sql-tuning: sqlplus
ACCESS sql-tuning: asks for nothing; importing grants none
LICENCE sql-tuning: allowed Apache-2.0
NOTICE sql-tuning: kept 1 files: NOTICE.txt
RECOMMEND database-sql: sql-tuning@1.2.0 (verified, by trust); not chosen: none
EVALUATED 1 skills from github.com/example/oracle-skills@0123456789ab sha256:…: 0 refused, 0 conflicts
- Technologies. Up to 8, each
<name>@<version>with a lower-case name such asoracle-database@19. A skill is compatible when its version range admits the version you give. Without any technology, noCOMPATIBILITYverdict is shown. - Project folder.
--repo <dir>(only for a source with a reviewed profile, see Vercel agent skills) reads the versions of the profile's technologies from thepackage.jsonfiles of that folder, relative to the current folder, instead of listing them. It cannot be combined with technology operands. Only dependencies are read. An exact version such as18.2.0is used as it is,^18.2.0gives18and~18.2.0gives18.2. A range such as>=18,<18,latestorworkspace:*, a missing version, or two different versions, giveunknown, so the verdict isunchecked. The reason is that the report must not claim a version it cannot read. If the scan hits a limit or cannot read a file, the command stops with aPROBLEMand no verdict. - Source scope. A source entry in
skill-sources.yamlcan state what upstream does not:scopewith up to 8capabilitiesand up to 8technologies(a name and a version range such as">=19"). A range the skill declares itself wins. The scope adds to the skill; it never removes one.
- id: oracle-skills
scope:
capabilities: [database-sql]
technologies:
oracle-database: ">=19"
- Access.
ACCESSlists what a skill asks for (network, credentials, tools). Importing grants none of it: no cloud credential or live database access is given by an import. - Licence and notices.
LICENCEis per skill, from the source'slicence.allow. A skill whose licence is not allowed isREFUSED. The attribution files of an allowed skill are listed asNOTICE, so the notices can be kept with it. - Overlap. For each capability that an imported skill and an installed skill both provide, the report uses the canonical selection.
RECOMMENDnames the winner and why it won;CONFLICTmeans no winner can be chosen;NONEmeans no skill is eligible. A recommendation is not applied: nothing is pinned or changed. - Exit codes. 0 when every skill passes. 1 when a skill is refused or blocked by a high finding, its licence is not allowed, its compatibility is
incompatibleorunchecked, an installed skill fails the integrity check, or a conflict is found. For a source with a reviewed profile, 1 also when a skill is blocked by the profile, or a folder the profile selects is missing from the import. APROBLEMline also exits 1 and names the file or field. - Limits. The same fetch limits as
scf skills import. An evaluation that is interrupted (Ctrl-C) printsPROBLEMand writes nothing.
Vercel agent skills#
The connector vercel-agent-skills (https://github.com/vercel-labs/agent-skills) is the first source with a reviewed profile. The profile is code, not data in skill-sources.yaml: it lists the folders the connector selects and what each one may offer. The entry in skill-sources.yaml is short:
- id: vercel-agent-skills
repository: https://github.com/vercel-labs/agent-skills
paths: [skills]
trust: verified
licence:
allow: [MIT]
ref:
approved: 0000000000000000000000000000000000000000 # placeholder: the reviewed commit goes here
Do not add a scope to it: a source with a profile is refused with PROBLEM. Its paths may list the selected folders instead of skills; a folder that is not listed is then not fetched at all, and it does not count toward the import limits.
- Selected folders.
frontendgroup:skills/react-best-practices,skills/composition-patterns,skills/react-native-skills.typescriptgroup: none yet.skills/web-design-guidelinesis listed but on hold, so it is never offered.skills/vercel-deploy-claimableis not selected: the report saysNOT-SELECTED skills/vercel-deploy-claimableand it is not evaluated, so its licence, findings and size do not change the verdict (its bytes still count toward the import limits whilepathsisskills). The profile's capabilities, technologies and category replace the ones the upstreamSKILL.mddeclares. - A declaration becomes reviewed only when its digest (the package digest of that folder, skill-relative paths) is in that folder's
reviewedlist infrontend-rules.ts. A list entry is added by a code change, after someone has read that exactSKILL.md. Each list is empty in this release, so no folder is reviewed yet. One changed byte gives a new digest, which is not reviewed, and a digest approved for one folder does not approve another. - Evaluation stays
missing. Each folder needs the evaluationskill-gateto have passed for its digest. No result can be recorded yet, soevaluation missingis shown for every folder and the verdict is blocked. Afailedresult also blocks. - Why
web-design-guidelinesis on hold. It takes its rules from a remote address when it runs, so the content that is reviewed is not the content that runs. - Size. A skill body over 5000 tokens is
too large, and is blocked. - What a blocked skill does. The
CONNECTORline names the group, whether the declaration is reviewed, the evaluation state and any hold. Each skill that is blocked is not chosen as a recommendation, and the source verdict exits 1.
For example, the lines of an import in which one selected folder is missing:
CONNECTOR vercel-agent-skills react-best-practices: group=frontend declaration unreviewed; evaluation missing
MISSING skills/composition-patterns: the profile selects it; the import does not have it
NOT-SELECTED skills/vercel-deploy-claimable: the profile does not select it; not evaluated
When the import stages nothing, the report lists each selected folder as REFUSED (with its reasons) or MISSING, each other folder as NOT-SELECTED, and ends with EVALUATED 0 skills … 0 refused or N refused. The exit code is 1.
Upstream facts were not checked when the profile was written. Folder names, the next and react-native ranges, and the remote fetch of web-design-guidelines need a human check against the repository before any digest is added.
Check a promotion#
scf skills promote check <source-id> [<exceptions-file>] shows, for each skill of the staged import, whether its licence, its provenance and its findings allow promotion. It is offline and read-only, like Scan an import: nothing is written, loaded, pinned or run, and no network or process is used. Promotion itself is not done by this command.
scf skills promote check example-skills
PROMOTABLE example/skill-a skills/skill-a MIT
REFUSED skills/skill-b: missing (skills/skill-b: no licence file for this skill or a folder above it)
CHECKED 2 skills from github.com/example/skills@0123456789ab sha256:…: 1 promotable, 1 refused
- What it checks. The staged import is read and checked as in Scan an import. Then, per skill path: the licence and notice evidence of the commit is checked against the source's licence allow list, the skill is normalized, the scanner runs with the exceptions, and the provenance is recorded.
- Lines.
PROMOTABLE <id> <path> <licences>for a path that may be promoted, with its licence names.REFUSED <path>: <status> (<reasons>)for every other path. The status is the licence status (for examplemissing,ambiguous,revieworrejected),blockedwhen a high finding is not accepted, ornot-normalizedwhen the normalization step rejects the skill, orevaluation (<suite>: <codes>)when a suite ofskill-evaluations.yamlis not passed for the digest (Confluent skills). AnEVALUATIONline per suite follows the other lines. AnEFFECT <id> <digest>: <verdict>line per skill follows, with the effect code in brackets when the rule refuses it, or withaccepted by <owner> on <date>when a decision was used. A path whose effect is refused printsREFUSED <path>: effect (<code>), with the codes of Score and gate an import. TheADDRESSlines of Scan an import follow them, then oneCHECKEDline with the counts. - What never is promotable. A path with a missing, ambiguous, review or rejected licence, with an open high finding, or with a suite that is not passed for its digest, or with an effect that the rule of Score and gate an import does not accept. An import without licence evidence refuses every path with
missing, so an import written before #511 cannot pass this check. - Reasons. Reasons never show credential-like text; it shows as
(hidden). Licence texts are never printed. - How a licence is recognised. The text is compared word for word, without case, punctuation, indentation or line breaks, so a re-indented or re-wrapped text, or a text on one line, is the same text. MIT, ISC, 0BSD, BSD-2/3-Clause and Unlicense must match the complete text; owner notices (
Copyright (c) 2024 Example Person, up to 7 words, however wrapped) and a title line may differ. Apache 2.0 must match the published text, with or without the appendix, with the placeholderCopyright {yyyy} {name of copyright owner}orCopyright [yyyy] [name of copyright owner]or a filled-in owner, and withhttporhttpsaddresses. A notice that adds a restriction, and any other changed word, givesambiguous … the licence is not recognised. - Exit codes. 0 when every path is promotable. 1 when a path is refused or a
PROBLEMis printed (for example a missing import, an unknown source, a source that is not enabled, an invalid exception file, or an invalidskill-evaluations.yaml). - Manifest fields. The promotion manifest (schema 1) lists the provenance of each promoted package. Two optional fields record who vouches for it:
trustis the trust tier of the source (for examplecommunity) and is always set;reviewholdsby,onandcommitof the source's review and is set only when the source has one. The review never holds the licence. Manifests written before these fields stay valid, andattributionProblemworks on them as before.
Licence review and provider assumptions#
A source that needs a licence review (the anthropic-skills connector, or any source with licence.reviewed) is promoted only when its licence.reviewed is the digest of the licence evidence a person has reviewed. promote check prints the digest and the state of the review:
LICENCES sha256:… 3 licence and notice files: not reviewed; check the lines above, then set licence.reviewed to this digest
- What the digest covers. Every licence and notice file that can apply to a skill folder of the import, with its path, its kind and its SHA-256 (or its problem, when it cannot be read), and the decision for each skill folder: its status, licences, declared licence and the file that governs it. It does not cover the commit id or the attribution text.
- When it changes. Any licence or notice file in or above a skill folder changes, appears or disappears, including the repository's root
LICENSE. A licence-like file in a parent folder counts even when it governs no skill. Also when a skill folder is added or removed, or the licence decision of a folder changes (for example itslicense:line inSKILL.md). - How to review. Read the lines
promote checkprinted above theLICENCESline, then setlicence.reviewedto the printed digest inskill-sources.yaml, by pull request. The statenot reviewedmeans the field is missing;changed since the reviewmeans it is an older digest. - Refusals. Until the digest matches, every path of the source is refused with status
review, andpromote checkexits 1. - Example entry. Placeholders are shown in angle brackets; use the commit you reviewed and the digest that
promote checkprinted for it:
- id: anthropic-skills
repository: https://github.com/anthropics/skills
status: enabled
ref:
approved: <full commit id>
paths: [skills]
adapter: agent-skills
trust: verified
licence:
allow: [Apache-2.0]
reviewed: sha256:<64 hex digits>
update: { cadence: manual }
owner: "@acme/skills"
The document skills of that repository are source-available, not open source. Their licence is not in allow, so they are refused as rejected or ambiguous; the Apache-2.0 skills can be promoted. Whether the upstream layout and licence text match what the check expects is not verified by the tests: a skill whose licence cannot be identified is refused, never promoted.
- Provider assumptions.
ASSUMES <id>: <codes>is printed for each skill that has any. They are found in the tools the skill declares, itscompatibilityand its instructions:claude-tools: a Claude tool name, such asWebFetch, orReadnext to the word "tool".mcp-servers: an MCP tool (mcp__…), declared or named in the instructions.claude-surface: the Claude apps (claude.ai, Claude Code, Claude Desktop, or the skills folders of the apps).anthropic-api: the Anthropic API, its key variable or its SDK package.
- Assumptions grant nothing. They are a declaration. They never block a skill and never change its tool profile: an imported
allowed-toolsfield grants no tools. Reference files are not read.
Review an import#
scf skills review <source-id> [--markdown] prints one report for a staged import, to decide what could be added to the catalogue. It is offline and read-only, like Check a promotion. Nothing is promoted, pinned, locked or written; every skill stays community.
scf skills review example-skills
scf skills review example-skills --markdown > review.md
- Per skill. The id, upstream path, name, digest and fingerprint; the category and where it came from (
declared,nameordefault); the licence verdict (allowed,review,rejected,missingorambiguous) with its reasons; the attribution files with their SHA-256; the author notice lines; and the scan result, blocked or not. - Collisions. Compared with the built-in and administrator skills, not with the repository. Each line has the collision id (
col-…), its class, reason, whether it blocks, its members and the shared capabilities. A skill whose upstream name is the id of an existing skill is shown assame name, even when the content differs (for example an upstreamjavaagainst the built-injava). - Categories. Skills are grouped by SCF category, with the existing skills of that category listed above them, so language, interface (API), testing and review candidates can be compared. An upstream skill without a category gets one from its name when a part of the name matches.
- Output. A
REVIEWline, then per categoryCATEGORY,SKILL,LICENCE,ATTRIBUTION,NOTICE,SCAN,NAMEandCOLLISIONlines,PROBLEMlines and a closingREVIEWEDline that saysnothing was promoted. The same input gives the same bytes. Credential-like text shows as(hidden). - Markdown.
--markdownprints the same facts as a table per category with notes below it, for a pull-request body. The output is bounded to 60,000 bytes; when it is cut, the last line says how many lines were left out. - Limits. More than 1,000 skills to compare prints a
PROBLEMand checks no collisions. - Exit codes. 1 when a skill is blocked, a licence is not
allowed, a blocking collision exists or aPROBLEMis printed (for example a missing import or an unknown source); 0 otherwise.
Onboard the community Spring source#
github.com/jiangxianze/spring-agent-skills is a community repository, so its source must be trust: community. It is added by pull request, in the order below. These steps promote nothing: compare only prints the decision, and no skill is installed, pinned or offered to a run.
Review. A person checks the maintainer, the licence and the commit to approve.
Record the review in
skill-sources.yaml. Setstatus: enabled,ref.approvedto the reviewed commit andlicence.allowto the licences it accepts. Addreviewwithby,on,commit(the same commit) andlicence(one oflicence.allow). Start from the example in Skill library sources.Check the file.
scf skills sources checkmust printOKand exit 0.Import.
scf skills import spring-agent-skillsfetches the commit into quarantine and keeps its licence files, whichcompareneeds.Scan.
scf skills scan spring-agent-skills. Each reviewed high finding is accepted by its id and digest inskill-scan-exceptions.yaml.Record the fixture results. Add one record per candidate skill to
skill-profile-evaluations.yaml. Take the id and the digest fromscf skills profiles evaluate spring-agent-skills:schema: 1 evaluations: - skill: java-code-review digest: sha256:… kind: fixtures verdict: passed # or failed owner: "@octocat" reviewed: 2026-10-14 expires: 2027-04-14A record is for one digest, so a new upstream version needs a new record. The other fields follow Evaluation records.
Compare.
scf skills compare spring-agent-skillsprints aPROMOTABLEorREFUSEDline for each candidate. It exits 0 only when every candidate isPROMOTABLE. A refused candidate lists its codes, which name the step still missing.
Update a source#
scf skills update <source-id> [--owner <@user>] [--reviewer <@user>] [--licence <id>] finds the upstream commit of a source, stages it with the same checks as scf skills import, builds the review report and opens a pull request that proposes the new commit. The pull request is the only result. Nothing is approved, promoted, pinned or released until it is merged.
Run it in the clone of the catalogue repository, in the folder that holds skill-sources.yaml. Its origin must be a GitHub repository, and gh must be installed and signed in (FACTORY_GH_BIN names another gh).
scf skills update awesome-copilot --owner @maintainer --reviewer @reviewer
- Entry or connector. A source with an entry must be
enabled;disabled,quarantinedandretiredare refused. A connector with no entry yet (for exampleawesome-copilot) needs--owner: the first run adds its entry, with the commit, the paths and the licence from the connector.--owneris refused for an entry that already exists. - Tracked ref. The upstream commit is that of
ref.track: a branch or a tag (an annotated tag gives the commit it points at). A name that is both a branch and a tag is refused. Withoutref.track, the commit is HEAD. - Reviewer. An enabled community source needs
--reviewer <@user>for a new commit: the entry gets areview(by,ontoday,commitandlicence).--licence <id>names the licence whenlicence.allowhas more than one and there is no earlier review. A run that printsUNCHANGEDneeds no reviewer. - Where the change goes. The branch is
skill-update/<source-id>-<commit>(12 characters of the commit). Onlyref.approvedandreviewof that entry change, or one new entry at the end ofsources:. Every other byte of the file stays. The edited file must pass the same rules asscf skills sources check, or nothing is pushed. - Pull request. The title is
Skill source <id>: update to <commit>(orfirst import at <commit>). The body holds the old and the new commit, the tracked ref, the selected paths, the verdict and the review report: licence, attribution, collisions and scan. It holds no text of the upstream skills. - Changes since the approved commit. For an update, the body has a
### Changes since <old commit>section between the header and the review report:- Each skill root is
added,removed,changed(its digest differs) orunchanged. Per changed skill: the paths added, removed and changed (by SHA-256), the name, version, category and capabilities as old → new, the licence status and licence ids, the scanblockedflag and the high and total finding counts, and the evaluation. - Evaluation. The verdict of the newest record for the old digest is shown, and whether a record exists for the new digest. Otherwise the line says
needs a new record. A changed digest never inherits the old verdict. - Canonical skills. For the capabilities of added or changed skills, the current canonical winners of the catalogue are listed (the same selection as
scf skills overlap). A row that adds aCONFLICTis marked(new conflict). - Verdict. The verdict line reads
needs attention: <parts>when security, licence, evaluation or review fails, and says that nothing is promoted until the gates pass. - Old commit not comparable. When the old commit is not in quarantine, it is staged first with the checks of
scf skills import. If that fails, the section says the old commit could not be compared, and the pull request is still opened. A first import has no section. - Lists lengthen only up to a fixed number, then show
… and N more. The section has its own byte budget inside the pull request limit.
- Each skill root is
- Supersedes. Open pull requests of the same source for older commits are listed in the body under
Supersedes (these stay open and are not changed). The run never closes, merges, edits or approves them. Only the pull request of this commit is updated on a re-run. - Re-run. While the pull request of a commit is open, a re-run rebuilds its title and body from the proposal on its branch and edits the pull request with
gh pr edit, only when the text differs. It never opens a second pull request and never pushes the branch again. A re-run whoseskill-sources.yamldiffers from origin, or whose branch no longer holds this proposal, is refused or left as it is (see Output). - Output. One line per outcome. A
CHANGES since <old commit>: <n> changed, <n> added, <n> removed, <n> unchangedline comes before theREVIEWEDline when the old commit was compared, orCHANGES the old commit <commit> could not be compared:UNCHANGED <id> is at <commit> (ref.approved); nothing to propose: the upstream commit is already approved. Nothing is opened.UPDATED <url> for <id>@<commit>; nothing is approved until it is merged: the pull request of this commit is open and its title or body was rebuilt and edited.OPEN <url> for <id>@<commit>; no second pull request was opened: the pull request of this commit is open and its text is already current. A branch that does not hold this proposal also givesOPEN, and the text is not changed.OK opened <url> for <id>@<commit>; nothing is approved until it is merged: the new pull request.PROBLEM <sentence>and exit 1: an unreachable upstream, a failed push orghcall, a refused source or option. The sentence is fixed text; git's and gh's own messages are never printed. A failed run leaves no branch behind onorigin. When the branch cannot be removed, thePROBLEMnames it: delete it by hand. A failedgh pr editprintsPROBLEMand leaves the pull request as it was.
- Ctrl-C. The update stops and, as far as it has pushed, removes its own branch. Nothing is proposed.
- Merging. Merging the pull request is the approval. Until then the working copy's
skill-sources.yamlon the default branch does not change.
Walk-through for the awesome-copilot connector:
- Propose.
scf skills update awesome-copilot --owner @maintainer --reviewer @reviewer. The first run prints the pull request address and theREVIEWEDline of the report. - Review. Read the pull request: the changes since the approved commit, the licence (
MITfor this connector), the attribution, the collisions and the scan in the body. Check that the reviewer named is the one who checked them. - Merge. The source is then pinned by the merged file.
scf skills import awesome-copilotfetches that commit, andscf skills review awesome-copilotshows the report again. - Next update. When upstream moves, run
scf skills update awesome-copilot --reviewer @revieweragain. While upstream is unchanged it printsUNCHANGED.
Check every due source#
scf skills update --due [--all] checks every enabled source whose update cadence is due, one after the other, and gives each the same outcome as scf skills update <source-id>. Use it on a schedule, so no one has to remember each library.
scf skills update --due
scf skills update --due --all
- Which sources.
--duetakes the enabled sources whoseupdate.cadenceis notmanualand is due. A source is due when it was never checked, when its last check ended withPROBLEM(it is retried on the next run), or when its last check is at least 1 day (daily), 7 days (weekly) or 30 days (monthly) old.--allchecks every enabled source and ignores the cadence.manualsources are never due by--due; they are checked only byscf skills update <source-id>.--duecannot be used with a source id, and takes no--owner,--revieweror--licence: a community source names its reviewer inupdate.reviewer. - The list. The sources are read from
skill-sources.yamlonorigin/<default branch>, not from the working copy. A clone that is behind origin needs nogit pull. A file that is not on origin, or that does not passscf skills sources check, stops the run with onePROBLEMline per error. - Community sources. For a new commit of an enabled community source, the pull request names
update.revieweras the reviewer. The reviewer is asked on GitHub after the pull request is opened. The pull request says that the review has not been done yet; do not merge before it is. A licence that is not clear fromlicence.allowstill needs a person: the source printsPROBLEM … needs --licenceand nothing is opened. - One source fails, the others go on. Each source prints its lines prefixed with its id, for example
awesome-copilot: OPEN <url> for awesome-copilot@<commit>; no second pull request was opened. An unchanged source produces no branch and no pull request, and a second run over the same revision produces no second pull request. - Summary and exit code. The last line counts the outcomes:
CHECKED <n> sources: <n> UNCHANGED, <n> OPEN, <n> UPDATED, <n> OK, <n> PROBLEM, withnot dueandmanualcounts when some sources were skipped. The run exits 1 when any source printedPROBLEM, and also when it was interrupted (Ctrl-C). - Record of checks. After the check of each source ends, its time and outcome are written to
skill-update-checks.jsonin the data folder (schema 1, written atomically). A missing or unreadable file means every source counts as never checked; the run prints oneNOTEfor it and goes on. The file is not in the repository and holds no secrets.
Schedule it with cron, run from the clone of the catalogue repository (the folder that holds skill-sources.yaml). Give scf by its full path, and check the log:
# crontab -e: every day at 06:00
0 6 * * * cd /Users/you/catalogue && /usr/local/bin/scf skills update --due >> /Users/you/Library/Logs/scf-skill-update.log 2>&1
On macOS, launchd runs it the same way. Save this as ~/Library/LaunchAgents/com.example.scf-skill-update.plist and load it with launchctl load:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.scf-skill-update</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>cd /Users/you/catalogue && /usr/local/bin/scf skills update --due</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>6</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/you/Library/Logs/scf-skill-update.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/Library/Logs/scf-skill-update.log</string>
</dict>
</plist>
gh must be installed and signed in for the user that runs the job, the same as for a single source.
Let the server run it#
The Foundry server can run the due checks itself, so no cron job or launchd job is needed. Name the catalogue clone and how often the server looks in config.yaml, or on the Skills card of Settings:
skills:
update_check:
folder: /Users/you/catalogue # absolute path of the clone; leave it out to turn the checks off
every: 1h # how often the server looks: 5m to 24h, default 1h
- Off by default. Without
folderthe server runs no check. Existing configurations need no change. - The folder. It must be an absolute path to a clone of the catalogue repository, the folder that holds
skill-sources.yaml. It must not be inside a.claudeor.codexfolder (the personal agent folders). A folder that does not exist, is not a folder or is not in a git work tree is reported as a problem, and nothing is checked. every. The server looks at the folder once a minute and starts a check wheneveryhas passed since the last one. It accepts<n>mor<n>hfrom5mto24h, for example30mor6h. Each source keeps its own cadence (see Which sources above): a source is checked only when it is due, as with--due. The first look comes about a minute after the server starts.- Log. Each line of the check goes to the server log with the prefix
skill-sources:, for exampleskill-sources: awesome-copilot: OPEN <url> …. Secrets are removed from the lines. - Problems. A
PROBLEMoutcome becomes a finding of the detectorskill-source-checkon the Problems page, named after its source (or Skill source checks for a problem of the whole catalogue). A later check that no longer reports it marks it as gone. Turning the checks off, or changing the folder, marks all of them as gone. - Changes. Changing or clearing the key takes effect at the next tick, without a restart. A check that is running when the folder is cleared or changed stops before its next git or gh step. A new folder is checked at the next free tick.
- Who may change it. Only an administrator can save these fields, in Settings (the Skills card) or in
config.yaml. - Git and gh. The server commits, pushes branches and opens pull requests in that clone with its own git and gh sign-in, the account that runs the server. It never merges or approves. Merging the pull request is still the approval.
ghmust be installed and signed in for that account. - Do not also run cron on the same clone. A cron job or launchd job and this setting would both check the same clone. Remove the cron or launchd job when you set
folder.
Score and gate an import#
Two commands decide which skills of a staged import may be promoted. scf skills score <source-id> [<suite-file>] scores every skill with the suite and keeps the result. scf skills gate <source-id> [<suite-file>] reads the newest result of each skill and prints the verdict. Neither calls a model or starts a process, and neither changes the import, the skill folders, skill-sources.yaml or the quarantine.
scf skills score example-skills
scf skills gate example-skills
SCORED kafka-consumer@1.2.0 new: selection=1.00 conflicts=1.00 context=1.00 tools=1.00
EVALUATED 1 skill from example/skills@0123456789ab sha256:… with suite sha256:…: 1 record written to skill-evaluations/example-skills/0123456789ab
PASSED kafka-consumer@1.2.0 new
GATED 1 skill from example/skills@0123456789ab sha256:… with suite sha256:…: 1 passed, 0 failed
The suite. Both commands read the suite file you name, or
skill-evaluation.yamlin the current folder. The gate checks each record against the suite it reads, so a record made with another suite isFAILED(suite-mismatch). A missing or invalid suite is aPROBLEM. Runscoreagain after you change the suite.TypeScript and Node.js suite.
evals/skill-gate/typescript-node/suite.yamlscores these skills offline (selection, conflicts, context and tools;scf skills effectuses itsinstructionssection, see Measure the effect of a skill). Its selection cases use three fixtures: a TypeScript Node.js service and a plain JavaScript package, where a skill is offered, and a Java library, where it is not. A skill is offered only when its id or a capability istypescriptornodejs, or when the task names it. Pass it as the suite file:scf skills score <source-id> evals/skill-gate/typescript-node/suite.yaml.Engineering-practice suite (testing).
evals/skill-gate/engineering-practice/suite.yamlscores skills of the testing practice: writing tests, test-first development and regression tests for a bug. Its nine selection cases use two fixtures, a Node.js package (js-cart) and a Java library (java-stock). A skill is offered when its id or capability istesting, or when the task names it. The other practices (planning, review, documentation, git, security and so on) have no suite yet, so this suite does not score them. Pass it as the suite file:scf skills score <source-id> evals/skill-gate/engineering-practice/suite.yaml. Itsinstructionssection has five scenarios forscf skills effect(see Measure the effect of a skill); each runs offline and needsjavacandjavafor the two Java scenarios.Records. Each run adds one record per skill under
<data folder>/skill-evaluations/<source>/<commit>/. Earlier records are kept. Files are written atomically with mode0600in0700folders. The newest record is the one written last.Catalogue description. When a skill has a
catalogue_descriptionin Skill selections,scorejudges itscontextdimension (the length check,description-cutanddescription-over-limit) on that text instead of the package's own. Itsselectiondimension is judged on the same text, for the candidate and for the imported skills it is compared with. The record stores the digest of the text it scored.gateandscf skills release publishrefuse withdescription-changedwhen the description is no longer the one scored, so runscoreagain after you change it. Without an override, both use the package's own text, as before.Mode. The gate picks the mode by itself:
new(no approved skill with the same id or a shared capability),upgrade(compared with the approved skill of the same id or the canonical winner of a shared capability), orduplicate(another staged skill shares a capability). A skill can have more than one mode; it passes only when every comparison passes.Lines.
PASSED <id>@<version> <mode>for a skill that may be promoted.FAILED <id>@<version> <mode>: <reason codes>for one that may not. Reason codes name the cause, for examplerecord-invalid,digest-mismatch,suite-mismatch,safety-blocked,licence-not-allowed,below-threshold,baseline-missing,regression,description-changedor an effect code (see Effect below); a dimension name follows the code where it applies.PROBLEMnames the file or field that stops the command.GATEDgives the counts.Missing or unreadable records. A skill with no record, or a record that cannot be read, has the wrong mode or digest, or belongs to another skill, source or commit, is
FAILED. It is never passed. Runscf skills score <source-id>to make the record.Effect.
gatealso reads the newest effect record of each skill, for its id and its package digest (see Measure the effect of a skill).scf skills promote checkapplies the same rule. Each verdict is:helps: nothing is added.harms: alwaysFAILEDwitheffect-harms. A decision cannot override it.no-measurable-effect:FAILEDwitheffect-no-effect, unless a decision withaccept: no-effectexists for the digest.- No record:
FAILEDwitheffect-not-measured, unless a decision withaccept: not-measuredexists for the digest. - A record that cannot be read or is invalid: always
FAILEDwitheffect-invalid.
A decision is your recorded answer to a skill whose effect is not measurable or was never measured. Put it in
skill-evaluations.yaml, in the top-leveleffect_decisionslist (the file may omit the list):schema: 1 evaluations: [] effect_decisions: - skill: kafka-consumer digest: sha256:<the package digest of the skill> accept: not-measured # or no-effect owner: "@octocat" # a GitHub user or team reviewed: 2026-10-13 # a date, not in the future note: Read the sample tasks; the skill has no measurable effect on them. # required, 1 to 200 characters- Rules. Each entry needs all six fields. A duplicate (same skill, digest and
accept), a bad value or an unknown key is aPROBLEMline. A decision applies to one digest only, so a changed package needs a new decision. - Output. A decision that was used prints
DECISION <id>@<version> effect <accept> accepted by <owner> on <reviewed>after the skill'sPASSEDorFAILEDline. An invalidskill-evaluations.yamlprintsPROBLEMlines and exits 1. - Codes. The effect codes are added to the reason codes of a skill. They are not evaluation reason codes, and the evaluation records do not change.
Exit codes.
scoreexits 1 when aPROBLEMis printed.gateexits 0 only when every skill passes and noPROBLEMis printed.Limits. A record is at most 2 MiB and a folder holds at most 5,000 files. A source that is not enabled is a
PROBLEMforgate.Not done. The gate prints the verdict only. No run or promotion reads it yet.
The Java and Spring Boot suite#
evals/skill-gate/java-spring/suite.yaml is the suite for the Java and Spring skills (java, spring-boot, and the others of that area). It has nine selection cases, a conflicts, context and tools section (the same as evals/skill-gate/suite.yaml), and an instructions section with four effect scenarios (The Java and Spring effect scenarios). The section is not mandatory, and plain score and gate do not run it. Run it like any suite:
scf skills score <source-id> evals/skill-gate/java-spring/suite.yaml
scf skills gate <source-id> evals/skill-gate/java-spring/suite.yaml
- What it checks. A skill must be offered for Java work and must not be offered elsewhere. Seven
offeredcases use tasks onfixtures/orders-service, a small Spring Boot service with a JPA repository, tests and a Mavenpom.xml. Twonot-offeredcases usefixtures/web-tool, a Node.js package with no Java in it. - When a skill is offered. The profile reads two names from a project:
java(apom.xmlorbuild.gradlewith.javasources) andspring-boot(a dependency namedorg.springframework.boot:*). A skill is offered when its id or one of its capabilities is one of these names, or when a task names one as a whole word. The Spring Boot fixture gives both names, so every Java and Spring skill is offered there. A plain Java library gives onlyjava, so it is not used for the selection cases. The effect scenarios use it. - Fixtures. Each fixture is a small folder with no network access, no credentials, no symbolic links and no
.claude/or.mcp.json.orders-servicecompiles with the plain JDK against the stand-in types in itssrc/stubs/javafolder, so its tests run without Maven.cache-libraryis a Gradle project with a plainmaintest that compiles withjavac, so Gradle is not needed to run it.web-toolruns withnode --test. The repository test (tests/skill-suite-java-spring.test.ts) checks the fixtures, and calls no model. - Not in this suite. There is no web starter (
spring-boot-starter-web). The selection cases do not usecache-library.
The Java and Spring effect scenarios#
The instructions section of evals/skill-gate/java-spring/suite.yaml has four scenarios for scf skills effect. Each one gives a task to an agent in a copy of a fixture, and a check decides whether the result is right. The check runs offline, with only the JDK and the stand-in types in src/stubs/java. Run them like the Kafka example:
scf skills effect <source-id> java evals/skill-gate/java-spring/suite.yaml
scf skills effect <source-id> spring-boot evals/skill-gate/java-spring/suite.yaml
| Scenario | Fixture | Area | What the check decides |
|---|---|---|---|
cancel-rules | orders-service | Spring | OrderService.cancel throws IllegalStateException for a PAID order and leaves it unchanged; throws NoSuchElementException for an unknown id; cancels and saves an OPEN order. |
optimistic-locking | orders-service | JPA | Order has exactly one @Version field, of type long, Long, int or Integer, and is still an @Entity with an @Id. |
single-load | cache-library | Concurrency | LoadingCache.get calls the loader once per key, also when two threads ask at the same time, and gives both the loaded value. |
log-load-failure | cache-library | java.util.logging | When the loader fails, a WARNING on the class logger carries the key and the thrown exception, and the same exception is rethrown. A load that works logs no warning. |
- Cost. The number of agent sessions is offered scenarios ×
repeat× 2, withrepeat: 4in this suite. A Java skill is offered on all four scenarios, so 4 × 4 × 2 = 32 sessions. A Spring Boot skill is offered only on the twoorders-servicescenarios, so 2 × 4 × 2 = 16 sessions. - JDK requirement. Each check compiles and runs Java with
javacandjava. Put a JDK 17 or later on thePATHof the terminal that starts the Foundry, installed outside your home folder, for example under/Library/Java/JavaVirtualMachinesor Homebrew's/opt/homebrew. The check runs in the sandbox, where the home folder cannot be read, so a JDK under your home folder (for example from SDKMAN) is not found. Check it withjavac -versionandjava -version. Without a JDK every check exits 1 in both arms, so the verdict does not measure the skill. - Tests.
tests/skill-effect-java-spring.test.tschecks the scenarios without a model. It checks that every check fails (exit 1) on the unchanged fixtures, also when the expected keywords are only in comments, and passes (exit 0) on a reference solution. It also checks which skills are offered on each scenario, and that thecache-librarytest passes with the plain JDK. It is skipped with a warning whenjavacis not found.
The suite file and its instructions section#
The suite file has one section per dimension (selection, conflicts, context, tools) and an optional instructions section for sandboxed agent scenarios. Plain score does not run them and writes no instructions scores, so without --agent the section only changes the suite digest and the gate's rules. score --agent runs them; see the next section.
schema: 1
instructions:
mandatory: true # default false: the gate then needs the instructions dimension
repeat: 3 # 3–20, default 3
scenarios: # 1–20 scenarios; scenarios × repeat is at most 200
- id: retry-consumer
repo: fixtures/kafka-consumer # a fixture folder next to the suite
task: Add a retry to the consumer.
check: The consumer retries once. # what the check looks for, at most 2000 characters
policy: { shell: true, network: false, filesystem: write }
timeout_sec: 900 # 60–3600, default 900
- Ids. Scenario ids are unique across the whole suite, including the ids of the other sections.
- Fixtures. A fixture folder may not hold
.claude/or.mcp.json(any letter case, any depth), and may not hold a symbolic link. Such a fixture is aPROBLEMatinstructions.scenarios (<id>) repo. The same limits as the other fixtures apply. - Digest. A suite without
instructionskeeps its digest. With it, the digest covers each scenario,mandatoryandrepeat, and a hash of each fixture: the sorted relative paths, the bytes and the executable bit of every file. Changing one assertion in a fixture changes the digest, so every record must be made again. - Gate. With
mandatory: true, a record without aninstructionsscore isFAILED(dimension-missing:instructions). The minimum number of samples isrepeat× the number of scenarios. Withmandatory: falsethe dimension is ignored when it is missing. - Paired baselines. A record may hold an
agentblock with the scores of its agent runs and of up to four baselines (the same skill at another digest). The gate uses a baseline paired in the record for every dimension, so no stored baseline record is needed. A stored baseline record of an older suite does not affect it. Without a pair, the gate behaves as before: no stored baseline record isbaseline-missing. - Refused records. A record whose
agentblock does not match itsdimensions.instructions, names an unknown scenario, or repeats a run id isrecord-invalid.
Run the scenarios in the sandbox#
scf skills score <source-id> [<suite-file>] --agent also runs the instructions scenarios of the suite with a Claude agent, and writes their scores into the record. It needs the macOS sandbox (sandbox-exec), a Claude model key and a suite with instructions. The offline scores are made as well.
scf skills score example-skills --agent
Illustrative output (the ids and scores are made up):
NOTE network: not host-limited; outbound is limited to port 443, not to the model's host
RUN kafka-consumer candidate kafka-consumer retry-consumer 1/3 run 6f1c…: 1
RUN kafka-consumer candidate kafka-consumer retry-consumer 2/3 run 9a0e…: 1
RUN kafka-consumer candidate kafka-consumer retry-consumer 3/3 run 2bd7…: 0 (skill-not-loaded)
SCORED kafka-consumer@1.2.0 new: selection=1.00 conflicts=1.00 context=1.00 tools=1.00 instructions=0.67
PASSED kafka-consumer@1.2.0 new
- Cost. A run is one agent session, plus a fixture copy and a check. The runs are scenarios ×
repeat× (1 + baselines). Each baseline is a skill ofgateChecks(at most 4). A candidate with two scenarios,repeat: 3and two baselines has 2 × 3 × 3 = 18 agent sessions. Runs are slow and cost model tokens. - Baselines. A baseline is run the same number of times as the candidate, with the same suite, and it is also scored offline. Its record is written into the candidate's
agentblock, so the gate can compare the two without a stored baseline record (see above). - Candidate that may not run. A candidate with promotion blockers, or with a licence that is not
allowed, printsSKIPPED <id>: …; no agent run. It gets the offline record only, and the gate then decides as it does without--agent. - Refusals. These print
PROBLEM …, exit 1 and write nothing. The sandbox is not available.SCF_USER_SANDBOXorFACTORY_USER_SANDBOXisoff. The suite has noinstructions. The agent is not Claude. The model key is missing. A fixture changed after the suite was digested. - Each run. A run scores 1 when its fixture step, its work step and its check succeed, and the skill was loaded into the session. Otherwise it scores 0 and prints the cause in brackets:
agent-failed,check-failedorskill-not-loaded. A fixture step that fails, or a fixture that cannot be read, is aPROBLEM <id>: …; no instructions, and that skill gets noinstructionsscore. - What the sandbox guarantees. Each arm (the candidate, then each baseline) runs one after another in its own temporary data folder. That folder holds only the arm's skill, pinned in its own lock file. A baseline is copied unchanged, and its digest is checked. Your real data folder gets only the evaluation record; its
skills/andskills.lock.jsondo not change. Every run has no GitHub token, an emptyghfolder, no personal Claude or MCP settings, a short environment, and a privateCLAUDE_CONFIG_DIRandHOME. The only credential it has is the model key. A check that needs a GitHub token or another stored secret fails. The agent may use only the tools of the scenario policy:Read,Glob,GrepandLSalways;Edit,WriteandMultiEditforfilesystem: write;Bashforshell: true;WebFetchandWebSearchfornetwork: true. - What it does not guarantee. Outbound network is limited to one port (the model's), not to the model's host, because the macOS sandbox cannot filter by host. The agent therefore sees the model key and can reach any host on that port. Only macOS is supported. The output says so with the
NOTEline, and the record says so inagent.network(not-host-limited) andagent.outbound. - Deadlines. The fixture step has 120 seconds, the work step has the scenario's
timeout_sec, and the check has 300 seconds. A deadline that passes ends that run. - Interruption. On Ctrl-C or SIGTERM, or on an error, every running job is cancelled and waited for, the temporary folders are removed and your environment is restored. It prints
PROBLEM the evaluation was interruptedand writes no record. Run it again to get a record. evaluateandscore.scf skills evaluate … --agentis refused with a message that points toscore --agent. Plainscf skills scoreandscf skills gateare unchanged.
Measure the effect of a skill#
scf skills effect answers one question: does a skill change what an agent does on the sample tasks? It runs the skill's instructions scenarios twice on the same model, once with the skill and once without it, and stores the verdict. It runs in the CLI process only, like score --agent, and it needs the same requirements: the macOS sandbox (sandbox-exec) and a Claude model key.
scf skills effect example-skills kafka-consumer
scf skills effect kafka-x@1.0.0
The first form is for a staged skill of an import. The second is for an approved, pinned skill of the registry. A skill that is not staged, not approved, not active or not pinned is refused with a PROBLEM line, and nothing runs.
What runs. Only the scenarios where the skill is offered: the skill catalogue, built from the scenario's fixture and task, lists the skill with evidence. This is the same choice the agent would see in a normal session. A scenario where the skill is not offered prints
SKIPPED <key> <scenario>: not offered; not run. Each offered scenario runsrepeattimes in each arm (theinstructions.repeatof the suite). The without arm runs the same tasks with skills off.Verdict. The pass rates of the two arms are compared with a two-sided Fisher exact test. The verdict is:
helpswhen p ≤ 0.05 and the with-arm pass rate is at least 0.2 higher (pass-rate-higher);harmswhen p ≤ 0.05 and it is at least 0.2 lower (pass-rate-lower);no-measurable-effectotherwise (not-significant).
If no scenario is offered, nothing runs and the verdict is
no-measurable-effect(never-selected). If scenarios are offered but the skill never loads in the with arm, both arms still run, and the verdict isnever-selectedas well.Tokens and time. They are reported only. They do not change the verdict.
Output. One
RUNline per run, oneSKIPPEDline per scenario that is not offered, then oneEFFECTline. Illustrative output (the numbers are made up):EFFECT example-skills@1.2.0 helps (pass-rate-higher): with 18/20, without 10/20, difference +0.40, p 0.0138; tokens 412000 vs 355000; time 610 s vs 540 s STORED skill-effects/kafka-consumer/3f9a…/effect--0000000001.jsonA refusal or an interrupt prints
PROBLEM …(andINTERRUPTEDon Ctrl-C, SIGTERM or an error), exits 1 and writes nothing.Where records go.
<data folder>/skill-effects/<id>/<digest>/effect--<sequence>.json. Each record holds the suite, the runs, the verdict and the runtime (Foundry and Node versions, agent and model). Records are only added, never replaced. The newest record of each skill and digest is read by undefined, undefined and theeffectcolumn ofscf skills. Ahelpsverdict passes.harmsalways blocks promotion.no-measurable-effectand a missing record need an owner decision (effect_decisionsinskill-evaluations.yaml), which is described under Score and gate an import.Cost. The number of agent sessions is offered scenarios ×
repeat× 2. The tokens are about that number times the tokens of one session. For the example suite inevals/skill-effect/kafka/, all 5 scenarios are offered, so 5 × 4 × 2 = 40 sessions.Example set.
evals/skill-effect/kafka/suite.yamlhas five Kafka tasks (idempotent producer, commit after processing, key by order, dead letter, retry with backoff) on thefixtures/ordersproject. It has no network and no credentials. Its checks are offline behaviour tests with stubbed clients.TypeScript and Node.js set.
evals/skill-gate/typescript-node/suite.yamlhas five tasks:parse-stock-event(a parser that checks its input and never throws),config-validation(the PORT and HOST rules),graceful-shutdown(close the server, then exit),report-stream-errors(stream a report and destroy the output on a read error) andawait-notifications(a bounded async send, in JavaScript). Four run onfixtures/ts-serviceand one onfixtures/js-package. It has no network and no credentials; each check is an offlinenodescript with stubs. The cost is 5 × 4 × 2 = 40 sessions. The checks need Node 22.18 or later, for type stripping of.tsfiles.
Skill identity and content fingerprint#
Each normalized skill has five identity terms. Nothing reads them yet, so they do not change what runs use.
id: the source-qualified id,<source>-<name>-<hash>. It changes with the source id.upstream:<repository>#<name>. The same for every source record of one repository.nameandversion: the name inSKILL.mdand its version. An undeclared version is0.0.0-g<commit>.fingerprint: a hash of the skill's content (sha256:…).digest: the raw package digest, as pins and locks use it.
What the fingerprint ignores
skill.yamlat the top of the skill, and.DS_Storefiles anywhere.- The folder name: paths are relative to the skill, so a renamed folder is the same skill.
- The Unicode form of a path (NFC or NFD).
- In
.mdand.txtfiles outsidescripts/andassets/: CRLF or LF line endings, and a byte-order mark. - In
SKILL.mdfrontmatter: key order, quoting and YAML comments. Values are compared after parsing. - The source, repository and commit.
What changes it
- Any instruction or frontmatter value in
SKILL.md, including indentation and trailing blank lines in the body. - Any change to a file under
scripts/,references/,assets/or at the root, includingLICENSE.txt,REVIEW.mdandevals/. Scripts, assets and other non-document files are compared byte for byte, so line endings there count. - Adding, removing, renaming or moving a file.
- A
SKILL.mdwhose frontmatter does not parse.
New version or new source record?
- Same source record, changed content: give it a new version. The same version with another fingerprint is a reused version and is reported as
version-conflict. - Same version, same content, new commit: no new version is needed. The fingerprint does not change.
- Another repository (a mirror, a fork, a moved or renamed repository): it gets its own
idandupstream. It matches the original only by fingerprint. - A changed
nameinSKILL.md: this is a different skill, not a new version. - A changed source
idinskill-sources.yaml: all of its skills get new ids. - Neither: a renamed folder, or a changed
pathsentry.
How two skills relate (relateSkills, symmetric):
same-artifact: same fingerprint, same repository and name, same version.same-content: same fingerprint otherwise, such as a mirror or an unchanged fork.version-conflict: same repository and name and version, other content.other-version: same repository and name, other version and content.distinct: anything else. A skill with the same name in another repository is not linked unless the fingerprints match.
Supported Java and Spring versions#
A skill that declares a Java, Spring or build tool version is checked against the versions the Foundry supports. The project's own versions come from its build files.
| Technology | Supported lines |
|---|---|
| Java | 17, 21, 25 |
| Spring Boot | 4.0, 4.1 |
| Spring Framework | 7.0 |
| Maven | 3.9, 4.0 |
| Gradle | 8.14, 9 |
- Where the version is read from. Java:
java.version,maven.compiler.releaseormaven.compiler.sourceinpom.xml, andJavaLanguageVersionorsourceCompatibilityinbuild.gradle/build.gradle.kts. Spring Boot: the Maven parent or plugin, the Gradle pluginorg.springframework.boot, or a Boot BOM. Spring Framework: itsorg.springframework:dependencies. Maven and Gradle:distributionUrlin the wrapper files. A value such aslatest,3.+or^3.2gives no version. - Stale. A skill is stale when its declared range fits none of the supported lines (for example
spring-boot: ^2, orjava: 8). A stale skill is never chosen: it isincompatibleand an import of it is markedstale. A range that cannot be read isuncheckedand is not chosen either. - Aliases. Other names are read as the same technology:
jdkandopenjdkare Java,springbootis Spring Boot,springandspring-coreare Spring Framework,mvnandapache-mavenare Maven, andgradlewandgradle-wrapperare Gradle. - Changing the table. The table is data in
src/skills/technology-rules.ts. The owner changes it by pull request.
Skill collisions#
When two or more skills overlap, a collision is reported with one class, one reason and a stable id (col- and 16 hex characters). The result is the same for any input order. Nothing reads collisions in a run yet: runs do not build them, so skills are offered as before.
| Class | Reason | When |
|---|---|---|
exact-mirror | mirror.same-artifact | One repository, the same version and content, through more than one source record. |
exact-mirror | mirror.same-content | The same content from another repository, or under another version. |
exact-mirror | mirror.settings-differ | The same content, but the approved copies differ in skill.yaml settings (technologies, required tools, dependencies, provider, risk and so on). |
version-reuse | version.reused | The same skill and version with other content. |
id-conflict | id.other-version | The same skill at another version, with other content. |
id-conflict | id.other-upstream | The same id from another repository, with other content. |
fork | fork.declared-upstream | Other content, with a recorded link to the other repository. |
fork | fork.same-name | Other content under the same name in another repository. |
capability-overlap | capability.shared | Different skills that declare the same capability. The report lists the shared capabilities. |
- What is merged. Skills with the same content fingerprint are one entry. The entry keeps the representative skill unchanged, the source of every approved copy, and a count of the copies that are not approved. Skills with different content are never merged.
- What is held back. An entry is held back from the planner when its approved copies are in
version-reuse,id-conflictormirror.settings-differ. An unapproved copy is never offered, and it cannot hold back an approved one. Forks and capability overlaps stay offered, each under its own id. - Limits. A collision check covers at most 1,000 skills. A larger set is refused.
Overlap with the catalogue#
scf skills overlap <source-id> compares the staged import of a source with the skills already in the catalogue (built-in and administrator). For each capability the two share, it says which skill is canonical, or that the conflict is unresolved. It is offline and read-only, like Check a promotion: nothing is written, loaded, pinned or run, and no run reads its report.
scf skills overlap example-skills
CANONICAL code-review: review-guide@2.0.0 (trust-tier)
REJECTED community-review@0.4.0: outranked
CONFLICT kafka-producer: tie kafka-basics@1.0.0, kafka-producer-pro@1.0.0
NEW kafka-admin: kafka-admin@0.1.0 provider confluent
OVERLAP 6 skills from github.com/example/skills@0123456789ab sha256:…: 4 shared, 1 new, 2 vendor-only, 1 conflicts
- What it reads. The staged import, normalized, with the source profile applied (see Source profile), the registered skills, and
skill-selections.yaml(Skill selections). Registered skills from the repository are not compared. - Lines.
CANONICAL <capability>: <id@version> (<level>)names the winner and the level that decided it (trust-tier,specificity, and so on;only-candidatewhen nothing else competed). Each loser has aREJECTED <id@version>: <code>line under it.CONFLICT <capability>: <rule> <ids>names the skills that cannot be decided:tie(two skills tie on a high-risk capability and only age or name separates them),mandatory(a skill inskills.selection.includeis not the canonical one) orwinners-overlap(the best skill for one capability is not the best for another capability it also provides).NONE <capability>: no skill can be usedmeans every candidate is rejected.NEW <capability>: <id@version>is a capability only the source declares. A skill with provider requirements prints itsprovider <name>on each line.VERSIONS <skill>: <technology> <range>, …gives the declared versions of a skill, when it has any (Confluent skills). - Rejection codes.
safety(an open high finding of the scan, or a failed promotion blocker),licence(the licence is not allowed, orlicence.reviewedis missing or stale),toolsand the codes of the selection rules (revoked,quarantined,discovery-only,incompatible,compatibility-unchecked). A skill with a failed gate can never be canonical. - Vendor scope. A vendor-scoped capability (the source profile's
<provider>-<capability>) is shared only with a registered skill of the same provider, never with a portable skill. TheOVERLAPline says how many capabilities were kept vendor-only. A capability that is portable for one staged skill and vendor-scoped for another is aPROBLEM. - Problems.
PROBLEMlines name the skill and a fixed code:profile(the source profile cannot be applied),scope-collision,blocking-collision(a collision with another skill holds it back), a skill that could not be normalized, or a catalogue that could not be read in full. A damaged staged import prints a fixed text and no reason. - Exit codes. 0 when there is no conflict and no
PROBLEM. 1 for a conflict or aPROBLEM.NEW,NONEandREJECTEDlines alone do not change the exit code. - Order and text. The same input in any order prints the same lines. Only slugs, versions, codes and levels are printed: no description, name or path of an upstream skill.
- Limits. The staged skills and the registered skills together are at most 1,000. A larger set prints
PROBLEM too many skills or capabilities to compareand exits 1.
Discover candidate sources from Skills.sh#
scf skills discover [<export-file> [<file>]] proposes candidate sources from a saved Skills.sh export. It does not call Skills.sh and does not scrape it. Nothing it finds is approved, fetched or staged. You add a source to skill-sources.yaml by hand.
scf skills discover skills-sh-export.json
PROPOSED github.com/example/skills@0123456789ab (3 listed)
KNOWN github.com/example/other is in the source list (enabled)
UNRESOLVED github.com/example/gone: the repository could not be reached
- Export. A JSON array, an object with
skillsorrepositories, or one repository per line. Only https GitHub repositories are kept. Entries that look like a credential are dropped. Trust, licence, popularity and safety claims in the export are not copied. - Revision. Each repository is resolved to the commit its own
HEADpoints at (git ls-remote, 20 s each). A repository git cannot read isUNRESOLVEDand not stored. - Candidate list.
<data folder>/skill-source-candidates.json, with statusproposed, originskills.sh, repository and revision. It is the only file written. The allowlist, the lock, the quarantine and releases are never touched. - Already known. A repository already in
skill-sources.yaml, in any status, is shown asKNOWNand not added. A repository already in the candidate list is skipped. - Review. To use a candidate, review it and add it to
skill-sources.yaml; thenscf skills importfetches it. The revision shown is a proposal, not an approved commit. - List.
scf skills discoverwith no operand prints the stored candidates andOK <n> candidates. - Optional. Runs never read the candidate list. A missing list, a failed discovery or a missing export does not affect approved skills.
- Ctrl-C. The run stops and the list is not changed. A broken list is never overwritten.
- Exit codes. 0 on success. 1 for a
PROBLEMor a usage error. Limits: export 4 MiB, 20,000 entries, 100 repositories per run, 500 candidates in the list.
Skill selections#
skill-selections.yaml next to skill-sources.yaml records which skills of a source the maintainers have selected, with the product and SDK they target and what each one needs. It is changed by pull request and checked with the CLI. It grants nothing: no run reads the file itself, and a selected skill is not used because it is listed here. The one exception is catalogue_description (below), which reaches runs only through a published release.
schema: 1
selections:
- source: microsoft-skills
skill: azure-cosmos-py # the name in SKILL.md
digest: sha256:… # the skill's digest in import.json
scope: provider # portable | provider
provider: azure # required for scope provider, refused for portable
product: { name: azure-cosmos-db, version: "2024-11" }
sdk: { name: azure-cosmos, language: python, version: ">=4.7" }
category: database
capabilities: [cosmos-db]
requires: { clis: [az], mcp: [azure], credentials: [azure-identity], services: [azure-cosmos-db] }
catalogue_description: "Azure Cosmos DB data access in Python: queries, partitions and throughput." # optional
owner: "@octocat"
reviewed: 2026-10-13
- Fields.
sourceandskillname one skill of a source;source+skillis listed once.digestis the skill's digest inimport.json.scopeisportable(guidance for any provider) orprovider(specific to one provider).provideris a lower-case slug, required forproviderand refused forportable.product(name,version) andsdk(name,language,version) are required forprovider;sdk.nameis a package name such as@azure/cosmos.categoryandcapabilities(1 to 32 slugs) describe the skill.owneris a GitHub user or team;reviewedis a date that is not in the future. - Catalogue description.
catalogue_descriptionis an optional short description for the skill catalogue, used instead of the package's own when the skill is in the catalogue. Use it when the upstream description is too long or does not say what the skill is for. It is 1 to 160 characters, with no leading or trailing space, no control or invisible character and nothing that looks like a credential. It is tied todigest: an upstream update that changes the digest drops the override until the entry is reviewed again. The upstream package, its digest and its provenance do not change, and the instructions of the skill are not changed. The catalogue shows the override in place of the package's text, andscf skills scoreandgatejudge the skill on that text (see Score and gate an import).- Where it reaches runs.
scf skills release publishwrites the text into the release entry of the skill ascatalogueDescription(source: skill-selections). The release digest covers it. A release without the field keeps the package's own text. An older Foundry refuses a release that carries the field, because its manifest schema is strict. - Where it is shown.
scf skillsprintsDESCRIPTION <id>@<version> skill-selections <release>: <text>for each skill whose override came from an approved, verified release. The run record keeps up to 20 of the descriptions the agent was shown, with their release, and the "Why these skills" list of the run page shows them as plain text.
- Where it reaches runs.
- No wildcards. Names refuse
* ? [ ] { }, and versions refuse*and?: one entry names one skill and one version or range. Versions are text of at most 64 characters. - Requirements.
requireslists what the agent itself must call or reach during a run:clis,mcpservers,credentialsandservices. Each name is a lower-case slug. The total is at most 16 names, and a name may be listed only once across all four lists. Each name becomes a connector slug:cli-<name>,mcp-<name>,credential-<name>orservice-<name>(at most 64 characters).scf skills selections checkprints them sorted. - Check.
scf skills selections check [<file>]readsskill-selections.yamlin the current folder, or the file you name. It works offline and does not need the data folder. It prints one line per entry (source and skill, scope, provider, product, SDK, connectors, owner, reviewed date and the start of the digest), thenOK <n> selections, and exits 0. Otherwise it prints onePROBLEM <where>: <message>line per error and exits 1. A missing file is aPROBLEMfor this command (for readers, it means "no selections"). Credential-like values and control characters are never printed.- Overrides. An entry with
catalogue_descriptionalso needs its source to be enabled with an approved commit inskill-sources.yaml, and the skill must be staged with this digest at that commit. Otherwise the check printsPROBLEM selections[<n>] (<source>/<skill>) catalogue_description: …and exits 1. The check readsskill-sources.yamland the staged import only when an entry has an override; each line of an entry with one ends withdescription=<n> chars.
- Overrides. An entry with
- Limits. At most 500 selections. Every problem names its entry and field, for example
selections[0] (microsoft-skills/azure-cosmos-py) requires.mcp[0]. - Use in checks.
scf skills overlapandscf skills promote checkread this file to give capabilities to a skill that declares none, when its digest matches the staged skill. A skill with its own capabilities is not changed.
Confluent skills#
The Confluent agent skills (confluentinc/agent-skills) are added like any library source, from an example declaration that ships disabled. Their Kafka and Confluent skills must pass two evaluation suites before they can be promoted: messaging-semantics and compatibility.
- Copy the declaration. Copy the entry from
docs/examples/skill-sources.confluent.yaml(also in the installed package) intoskill-sources.yaml. At the commit you reviewed, by pull request:- replace the
placeholder/paths with the real folders; - check the upstream capability names under
profile.portableand the version ranges underscopeandprofile.products. The example was not checked against upstream; - set
ref.approvedto that full commit id, setstatus: enabledand removereason.
- replace the
- Run the checks.
scf skills import confluent-agent-skills, thenscf skills scan confluent-agent-skills,scf skills promote check confluent-agent-skillsandscf skills overlap confluent-agent-skills. - Give capabilities to a skill that declares none. Upstream skills may carry no capability metadata. For each such skill, add a
skill-selections.yamlentry with its digest fromimport.jsonand its capabilities (see Skill selections). - Write
skill-evaluations.yamlfrom theEVALUATIONlines ofpromote check. It is optional, and it is changed by pull request. Each record is one suite of one skill at one digest:
schema: 1
evaluations:
- source: confluent-agent-skills
skill: kafka-clients
digest: sha256:… # the skill's digest in import.json
suite: messaging-semantics
result: passed # passed or failed
owner: "@octocat"
reviewed: 2026-10-13
- source: confluent-agent-skills
skill: kafka-clients
digest: sha256:…
suite: compatibility
result: passed
project: [{ name: apache-kafka, version: 4.1.0 }] # the project's version of each technology the skill declares
owner: "@octocat"
reviewed: 2026-10-13
- Which skills need them. A skill with a
kafka-orconfluent-capability (including one the source scope adds) needs both suites. So does a skill without capabilities from aconfluentprovider source. - Fields.
projectis forcompatibilityonly and is refused formessaging-semantics.noteis optional text of up to 200 characters; text that looks like a credential is refused, and the message names the rule, not the text. A file has at most 500 records. A problem printsPROBLEMand exits 1. - A changed skill. A changed skill has a new digest. Its old records give
other-digest, so it needs new records and, when it declares none, a new selection.
- Read the result.
promote checkprints one line per suite of each skill that needs them:
EVALUATION confluent-agent-skills/kafka-clients messaging-semantics sha256:…: passed
EVALUATION confluent-agent-skills/streams-apps messaging-semantics sha256:…: not-recorded
passedmeans the suite passed for this digest. Otherwise the codes are listed, and the path isREFUSED <path>: evaluation (<suite>: <codes>)until they are gone. A messaging code ends withat <path>:<line>for the first lines that break the rule (ten at most, then+N more).- Records:
not-recorded(no record for the suite),other-digest(the record is for another digest),recorded-failed(result: failed),profile(the source profile or scope cannot be read; fix the declaration first). - Messaging codes:
exactly-once,ordering,keys,duplicates,retriesanddead-letters. Each code names a rule that a line of the upstream text breaks. The check reads text only, and upstream text is never printed. A passing text is not a review of the code, so read the skill as well. - Compatibility codes:
no-declared-versions(the skill declares no version),stale(a declared range admits only old versions, such as<=8),incompatible(the project version is outside the declared range) andunchecked(the project has no version for a declared technology). - Versions.
overlapprintsVERSIONS <skill>: <technology> <range>, …for each skill with a declared version: its own ranges, the source scope, or the profile products. A vendor skill shows the product ranges, for exampleapache-kafka >=4.0, confluent-platform >=8.0, flink >=1.19. - Overlap.
CANONICAL,CONFLICT,NEWandREJECTEDlines mean what they mean in Overlap with the catalogue. The summary line gives the counts.
Evaluate an import#
scf skills selections evaluate <source-id> [<selections-file>] says, for each selected skill of a staged import, whether it is eligible or held back, and how the skills overlap. It is offline and read-only: nothing is written, loaded, promoted or run, and no process is started. "Eligible" is a reported verdict, not an approval. Nothing reads the result during a run.
It reads skill-sources.yaml in the current folder, the staged import of the source and its stored licence evidence, skill-selections.yaml (or the file you name), skill-scan-exceptions.yaml, skill-overrides.yaml when present, and the skill registry. Run scf skills import <source-id> first; that is the only step that fetches.
Worked example: microsoft/skills. The entry below is a placeholder. The owner must check the upstream licence and choose the commit, and then replace ref.approved. The Foundry ships no approved Microsoft commit, and paths must list the folders that hold the skills (check the repository layout; if the skills are only reached through symbolic links, list the real folders).
schema: 1
sources:
- id: microsoft-skills
repository: https://github.com/microsoft/skills
status: enabled
ref:
approved: <full commit id, reviewed by the owner> # placeholder: replace it
track: main
paths: [skills] # the folders with the skills
adapter: agent-skills
trust: verified
licence: { allow: [MIT] }
update: { cadence: manual }
owner: "@your-team"
The selections name an Azure SDK skill and a Cosmos DB skill, and one portable guidance skill. The skill names are examples, not a list of what the repository holds.
schema: 1
selections:
- source: microsoft-skills
skill: azure-storage-blob-py
digest: sha256:… # from import.json, after the import
scope: provider
provider: azure
product: { name: azure-storage-blob, version: "12" }
sdk: { name: azure-storage-blob, language: python, version: ">=12.0" }
category: storage
capabilities: [blob-storage]
owner: "@your-team"
reviewed: 2026-10-13
- source: microsoft-skills
skill: azure-cosmos-py
digest: sha256:…
scope: provider
provider: azure
product: { name: azure-cosmos-db, version: "2024-11" }
sdk: { name: azure-cosmos, language: python, version: ">=4.7" }
category: database
capabilities: [cosmos-db]
requires: { clis: [az], mcp: [azure], credentials: [azure-identity], services: [azure-cosmos-db] }
owner: "@your-team"
reviewed: 2026-10-13
- source: microsoft-skills
skill: cloud-design-guidance
digest: sha256:…
scope: portable
category: architecture
capabilities: [cloud-design]
owner: "@your-team"
reviewed: 2026-10-13
Then, in order: scf skills sources check, scf skills import microsoft-skills (fetches the commit into quarantine), scf skills selections check, and scf skills selections evaluate microsoft-skills. The output looks like this; ids are shortened, and the real ids end in a hash:
ELIGIBLE microsoft-skills-azure-storage-blob-py-… provider azure-storage-blob@12 sdk azure-storage-blob@>=12.0
HELD microsoft-skills-azure-cosmos-py-…: needs-unavailable-tools
ELIGIBLE microsoft-skills-cloud-design-guidance-… portable - sdk -
EVALUATED 3 skills from github.com/microsoft/skills@0123456789ab: 2 eligible
- Eligible. A skill is
ELIGIBLEonly when its licence verdict from the stored evidence isallowed, it is selected with the current digest, and no reason below applies. The line shows the scope, the product and the SDK (-when the entry has none). - Held.
HELD <id>: <reason codes>lists every reason, sorted, and never stops the command:not-selected: no selection names the skill.selection-stale: the digest in the selection is not the skill's digest in the import.licence: the licence is not allowed by the stored evidence, or is unknown.needs-unavailable-tools: the skill requires a CLI, MCP server, credential or service that is not available here, or the import excluded it (MCP or plugin configuration, network use). The Cosmos DB skill is held this way.provider-content-in-portable: aportableskill contains provider-specific text.undeclared-requirement: provider-specific text needs a tool thatrequiresdoes not declare.provider-without-policy: aproviderskill would replace aportableone with the same capability, and no owner override names it. This command does not read project pins.blocking-collision,safety: an overlap blocks it, or the security scan blocks it.source-not-approved,incompatible,compatibility-unchecked: the source or the product version does not check out, or the compatibility is not known.not-normalized(in the output as a skill folder): the import did not normalize the skill; its folder is shown instead of an id.
- Overlaps. Each collision prints
OVERLAP <collision id> <class> <reason> <ids> [<shared capabilities>], after the skill lines. The same overlap rules as the rest of the catalogue apply. - Exit codes. 0 when the evaluation ran, including when skills are held. 1 when a
PROBLEMis printed or the usage is wrong. APROBLEMnames the file or field, and missing licence evidence asks you to import again. A missing selections file is aPROBLEM. - Credentials. Credential-like text is shown as
(hidden). - Limits. The registry must be complete. A missing or unreadable skill root, a refused root, an invalid package or too many registered skills stops the command with a
PROBLEMand exit 1, so the result is never partial.
Publish a catalogue release#
The catalogue owner keeps the catalogue versions to publish in skill-releases.yaml, next to skill-sources.yaml, in the catalogue repository. The file is changed by pull request and checked before the pull request is opened. It grants nothing: no run reads it, and publishing a version is a separate step.
schema: 1
repository: github.com/acme/scf-skills
releases:
- catalogue: scf-skills # skill id slug of the catalogue
version: 0.1.0 # skill version
skills: # 1 to 500 skills, each id once
- source: openhands-extensions
id: example-skill
review: # optional; publishing needs it
by: "@octocat"
on: 2026-10-13
revoked: # optional; each id must be in skills
- id: example-skill
version: 0.0.9
on: 2026-10-14
reason: Broken on Node 22
- Fields.
schemamust be1.repositoryisgithub.com/<owner>/<repo>, the same form assource.repositoryin a release manifest.releaseslists 1 to 200 entries. Eachcatalogueandversionpair appears at most once.catalogueand eachidare lower-case slugs of 1 to 64 characters;versionis a version like1.2.3. Eachskillsentry is{source, id}, and anidis listed once. - Review.
reviewhasby(a GitHub user or team, such as@octocator@acme/skills) andon(a real day, not after today). It is optional on purpose: an entry without it is valid here, and the publish command refuses it. - Revoked. Each revocation has
id,version,on(a real day, not after today) andreason: 1 to 200 characters, trimmed, with no control character. A revokedidmust be inskills, and it is listed once. - Unknown keys are refused.
- Reading. The file is read safely. A symbolic link, a file that is not a regular file, a file larger than 256 KiB and YAML with aliases are refused. Warnings from the YAML library are not printed, because they repeat file content.
scf skills releases check [<file>] checks the file offline and read-only. It reads skill-releases.yaml in the current folder, or the file you name. It works without the data folder. For each entry it prints the catalogue and version, the number of skills and revocations, and the review:
scf-skills@0.1.0 1 skills, 0 revoked, NOT REVIEWED
OK 1 releases for github.com/acme/scf-skills
Reviewed entries print reviewed by @octocat on 2026-10-13 in place of NOT REVIEWED. The last line says OK <n> releases for <repository> and the command exits 0. A problem prints one PROBLEM <where>: <message> line per error and exits 1. The message never repeats file content. A missing file prints PROBLEM the release request file does not exist and exits 1.
scf skills release publish <catalogue>@<version> publishes one of these entries. Run it in the catalogue repository, in the folder that holds skill-releases.yaml and skill-sources.yaml. It writes the release to skill-releases/<catalogue>/<version> in that folder, and changes nothing else: not the config, the pins or skills.lock.json.
scf skills release publish scf-skills@0.1.0
Publishing refuses, prints PROBLEM <c>@<v>: nothing was published and exits 1, and writes nothing when any of these holds:
- the file is missing or invalid, or the entry has no
review; - the folder has no git commit, or
skill-releases.yamlorskill-sources.yamlis not committed or differs from HEAD; - a listed source is unknown or not enabled, or has no staged import at its
ref.approved; - a listed skill is not staged or not normalized, or it is not promotable under the same checks as
scf skills promote check; - the newest evaluation record of a listed skill does not pass the same gate as
scf skills gate; - a revoked skill names another version than the built skill;
- the manifest or its credentials fail the release checks.
Every listed skill is checked, and each failure prints its own PROBLEM <source>/<id>: <reason> line. The two files must be committed and equal to HEAD, because the release records HEAD as its commit. Commit your edits first, then publish.
A revoked skill (an entry in revoked) skips the promotion and evaluation gates. It must still have a package that loads, an identified licence and a readable newest evaluation record, if there is one. Its verdict is what the record says (passed, failed), or not-run when there is no record. This lets you revoke a skill that is broken or that failed evaluation.
A successful run prints one SKILL line for each skill, then:
SKILL example-skill@0.1.0 sha256:… from openhands-extensions evaluation passed
PUBLISHED scf-skills@0.1.0 sha256:… 1 skills to skill-releases/scf-skills/0.1.0
APPROVE scf-skills 0.1.0 sha256:…
The same inputs always give the same digest, so publishing again with the same content prints UNCHANGED … already published and the APPROVE line again. A version can never be published with other content. A new evaluation record or a changed skill gives a different digest, and the command prints REFUSED <c>@<v>: already published with another digest (exit 1). Nothing changes. A folder that exists at that version but is not a valid release prints REFUSED … and the folder does not verify. A write that fails prints PROBLEM …; nothing was published and leaves no release folder.
To use the release, copy the APPROVE line into skills.releases.approved (catalogue, version and digest). The folder in skill-releases/ is loaded from there; you do not add it to skills.roots. Until you approve it, its skills do not load, unless skills.releases.allow_unreleased is on. Its packages are selectable at once, with the digests it lists; no pin is needed (see "Approval is the pin" in "Trust"). scf skills release list shows which releases are approved and which runs lock them. To require signatures, sign the release with scf skills release sign (see "Signed approvals" in "Trust").
Before you approve a newer release, see what it changes:
scf skills release diff scf-skills@0.1.0 scf-skills@0.2.0
It reads both releases from skill-releases/ and prints one line per change, with no network and no writes:
ADDED <id>@<version> <digest>andREMOVED <id>@<version> <digest>: a skill that is only in the new or only in the old release.CHANGED <id> <old version> <old digest> -> <new version> <new digest>: the same skill with another version or digest.DEPRECATED <id>@<version> <digest> revoked <day>: <reason>: a skill revoked in the new release, also when it was already revoked before ((already revoked)).CANONICAL <capability> <old> -> <new>: a capability that several skills provide and whose canonical winner changed. A side is<id>@<version>,conflict:<code>ornone.
The last line is DIFF … : <n> changes, or no changes. Both releases must belong to the same catalogue. A release that is missing or does not verify prints PROBLEM … and the command exits 1. The output is the same for the same input.
Propose the release to the deployment repository. Your deployed config lists the releases it accepts in skills.releases.approved. scf skills release propose adds the new one to that list in a pull request, so the change is reviewed before it reaches the deployment. Name the repository and the config file in your local config first:
skills:
releases:
deployment:
repository: owner/name # or host/owner/name
file: config.yaml # relative path to the YAML file that holds skills.releases.approved
Run it in a clone of that repository, where origin is the same repository:
scf skills release propose scf-skills@0.2.0
The command reads the release from skill-releases/. For a release you did not publish, give --artifact <absolute folder>: the folder is checked the same way as scf skills release adopt, and nothing is written to the store. Then it:
- reads the file from
origin/<default branch>and appends one entry (catalogue, version, digest) to the end ofskills.releases.approved. Every other byte stays, and the older entries stay, so locked runs keep resolving; - checks the edit: only the new entry is added, the file is valid YAML and the whole config passes the config schema;
- pushes the branch
skill-catalogue/<catalogue>-<version>and opens a pull request. Its body is the change report against the newest approved version of that catalogue: added, removed, changed, deprecated and canonical selection. That older release must be inskill-releases/, or named byartifacton its entry in your local config; otherwise the command stops before it pushes.
If skills.releases.keys is set, give --key <key-id> --signature <base64> with the values from scf skills release sign. The signature is checked against the digest before anything is pushed.
Output: a CHANGES line, then one of these:
OK opened <url> …: the pull request is open;OPEN <url> …: a pull request for that branch is already open, so no second one is opened;UNCHANGED … is already in <file>: nothing to propose;PROBLEM …(exit 1): nothing was proposed. If the command had pushed the branch, it deletes it again, as long as the branch still holds its commit.
The release must be newer than the newest approved version of that catalogue. The command never changes your local config, the data folder or the release store. Merging the pull request changes only the deployment file; this installation uses the release once its own config names it. It is not run on a schedule: run it by hand. A file the command cannot edit byte for byte (for example one with a layout it does not know) gives PROBLEM, and you add the entry by hand.
Known limits:
- The release digest is the digest of the Foundry-format package of each skill, not the import digest that records the name.
- With
core.autocrlfset, git can show a committed file as changed, and publishing then refuses it. Turn the setting off for the catalogue repository, or commit the files with the line endings they have on disk. - A crash during the write can leave a
skill-releases/.tmp-*folder. It is not a release and is never loaded. Delete it, then publish again.
Skill catalogue#
The catalogue is the short list of skills a planner may choose from. It holds only the most likely skills, not the whole library. For each skill it shows the id, version, a short description, the capabilities and up to two lines of evidence (for example "typescript: high confidence, 12 findings"). A skill with a provider block has its needs provider: … line first (see below). It never contains the SKILL.md text. The planner steps (plan and revise_plan) of issue-gitflow and issue-plan get it before their prompt, as a block marked as data. It lists only skills that are approved or built in, pinned, and that the resolver would select for a coder; skills that are not pinned are left out. The run log has one line per planner step, for example skill catalogue: 2 of 3 skills, 1 left out, about 180 tokens; 1 hidden (not pinned); the number hidden counts skills that would be offered if they were pinned. The step record has skillCatalogue with counts only, never the text. A failure to build the catalogue never stops planning: the log says skill catalogue: none (…) and the planner runs without it.
Upgrade note. Skills are now on for existing installs. A Claude session whose lock holds skills runs without your personal Claude setup (MCP servers, plugins, hooks and settings). To turn this off, set skills.builtin: false with no roots, or use skills: {mode: off} in a custom flow. The planner of the two flows now has only Read, Glob and Grep; the scoped git log, git show, git grep and ls Bash tools are gone, so the step is read-only by its tool list.
skills:
catalogue:
max_candidates: 20 # most skills in the catalogue (1–50, default 20)
max_tokens: 2000 # estimated size limit (100–20000, default 2000)
include: [] # skill ids that are always in the catalogue
exclude: [] # skill ids that are never in it; wins over include
- Ranking: by evidence in the repository profile, skill names found in the issue text, and the modules the work touches. The same input always gives the same list.
- Description. The description of a skill is the owner's
catalogue_descriptionwhen an approved, verified release carries one (see Skill selections); otherwise it is the package's own, cut to 160 characters. Ranking does not read the description; it changes only the text shown.scf skillsprints aDESCRIPTIONline for each override it shows, and the run record lists them under "Why these skills". - Spring Boot is not guessed from Java. The built-in
spring-bootskill needs a Spring Boot dependency in a build file, or sources that importorg.springframework.boot, in a module the work touches. Plain Java, plain Spring and other modules of the same repository do not get it, unless the issue names Spring Boot. It depends on thejavaskill, sojavais loaded with it (and counts towards the limits). - TypeScript is not guessed from JavaScript. The built-in
typescriptskill needs TypeScript sources (plustsconfig.jsonor atypescriptdependency as extra evidence). Work that touches only JavaScript files does not get it, unless the issue names TypeScript ortsconfig. - Node.js is not guessed from TypeScript or JavaScript. The built-in
nodejsskill needs evidence of the Node.js runtime in the module the work touches:engines.node,@types/nodeor a server package (such asexpressorfastify) inpackage.json, or an import of a Node.js built-in module. A browser-only package does not get it. - Kafka is not guessed from the word "events". The built-in
kafkaskill needs a Kafka client or schema registry dependency, a Kafka import, or akafka*.propertiesorkafka*.yamlfile in the modules the work touches, or an issue that names Kafka. Documentation or code that only mentions events, messages or queues does not get it. The skill is guidance only: it names no connector and gives no access to a cluster. It counts as high risk, so a run that needs it and cannot load it stops; see Missing and conflicting skills. - Oracle is not guessed from SQL. The built-in
oracleskill needs a sign of Oracle in the repository: an Oracle driver in a build file, anoracle.jdbcimport, PL/SQL package files (.pks,.pkb) ortnsnames.ora. Plain.sqlfiles, another database or SQL words in the issue do not bring it in; an issue that names Oracle or PL/SQL does. The skill is text only: it gives an agent no database access and holds no credentials. - Provider skills are not portable. A skill with a
providerblock (see Write a skill) has the evidence lineneeds provider: <name> (<products>; tools: <tools>), for exampleneeds provider: acme (kafka 3.7; tools: kafka-admin). The line is cut to 100 characters, names at most three products and three tools, then says "+N more". It stays in the entry when the entry is shortened, so a vendor-specific skill is never shown as if it were portable. - Too big: the lowest-ranked skills are dropped first, then descriptions are shortened. Skills in
includeare always included and never dropped. The catalogue says how many skills were left out. includeis not a pin. It does not approve content and does not make a skill selectable; see Pinned versions and integrity.- Settings apply to the whole installation, not to one repository. The config is refused if the always included skills do not fit the limits.
Skill request#
A final plan that is ready to code ends with one line, SKILL_REQUEST: {"version":1,"skills":[…]}. It names the skills the coder needs. Each skill has an id, a one-sentence reason and 1–5 evidence entries (catalogue:…, issue:… or path:<file in the repository>). At most 20 skills, each id once. A plan that needs no skill writes an empty list.
- In the posted plan: the plan comment gets a "Required skills" section (
None.when empty). The line itself is not posted and is removed from notes, send-back comments and created split issues. - Which plans: only plans that are ready to code. Plans that ask questions, are not code, or are too big do not carry a request. A request in the issue text or its comments is never copied.
- What fails the run: a line that is present but not valid (bad JSON, unknown version or key, too many skills, a bad id, reason or evidence, a repeated id, two request lines, a line over 8,000 bytes). The run stops at the plan gate (
risk_gateinissue-gitflow,post_planinissue-plan), before any coding, and nothing is posted. A plan with no line at all is accepted as an empty request. Start the run again to plan again. - Checked, locked and loaded. After the plan gate the run checks the ids; see Missing and conflicting skills. The planner picks from the skill catalogue; the run checks the ids, locks them and loads them into agent sessions (see Skills in Claude sessions and Skills in Codex and local-model sessions). A plan that picks nothing gives an empty request.
issue-code-dailyhas no plan gate, so it takes the request from the plan record, never from a comment; see Skills from the plan to the coding run.- Docker mode needs
nodein the image for the checking tool, ascreate-splitdoes.
Skill selection#
The resolver turns the skill ids of a plan into the exact skills a coder may get. It only returns skills that are approved and pinned; it never installs, downloads or grants anything. After the plan gate a run checks the plan's skills with it (see Missing and conflicting skills).
skills:
selection:
max_skills: 6 # most skills in one selection (1–20, default 6)
max_skill_tokens: 5000 # largest single skill (100–20000, default 5000)
max_tokens: 15000 # total for the set (100–100000, default 15000)
include: [] # skill ids that are always selected (mandatory)
exclude: [] # skill ids that are never selected; catalogue.exclude applies too
exclusive: [] # capabilities with one winner per run (at most 200, see "Exclusive capabilities")
- Exact versions: each id resolves to its active version, which must pass the pin check (see Pinned versions and integrity). Repository skills are never selected.
- Dependencies: a skill's declared dependencies are added before it (sorted by id) and count towards the skill and token limits. A dependency that is missing, too old (
min_version), excluded, unapproved or in a cycle refuses the skill that needs it. - Refused for: a role the skill does not allow, a size above
max_skill_tokens, a conflict with a skill already chosen (the earlier one stays), more thanmax_skills, or more thanmax_tokens. The order is always the same: mandatory ids first, then requested ids sorted by id. The set holds only what was asked for and what it needs. - Mandatory skills (
include) are never dropped quietly. If one is refused for any reason, or anincludeorexcludeentry is not a valid id or the list is too long, the whole selection is blocked: nothing is selected and the other skills are markedblocked. includeis not a pin. An included skill still needs an approved, pinned version. The config is refused if anincludeid is also inexcludeor incatalogue.exclude, or if there are moreincludeids thanmax_skills.- Decisions: every requested, mandatory and added skill gets a decision with a reason code:
mandatory,requestedordependencywhen selected;excluded,unknown,unapproved,unpinned,mismatch,unverified,role,too-large,dependency-cycle,dependency-unavailable,dependency-version,conflict,over-count,over-budgetorblockedwhen refused. The result also has the estimated tokens (description and SKILL.md text; reference files are not counted), so the size is known before an agent starts.
Skill lock of a run#
When a run has a plan that is ready to code, the first agent step resolves the plan's skill request and saves the result as skill-lock.json in the run folder. It names each skill by id@version and digest, with its source kind (admin or builtin), reason and evidence, and holds the plan hash and the repository commit. It holds no credentials, no paths and no issue text. run.json keeps a short copy (skillLock).
- Resume: a resumed run uses the locked versions, not the newest ones. The lock is made once per plan; if the run plans again, a new lock is made.
- Newer releases do not break it. When a newer catalogue version is published and approved, the older approved release stays in
skill-releases/and inskills.releases.approved. A run locked to its packages still resumes: the old packages are found by exactid@versionand digest, and they are not chosen for new plans, where the newest approved packages are active. Removing the old release fromskills.releases.approvedmakes the lock fail withskill integrity: … is missing. - Checked before every agent session: each locked skill must still exist at that version, be approved and pinned, and have the same digest. If not, the step stops before the agent starts, with a reason that begins
skill integrity:. A missing, changed or invalidskill-lock.jsonstops it the same way. To go on, restore the exact package and pin, or plan again, then resume.scf skills release listshows which runs lock a stored release, so you can see who depends on it before you changeskills.releases.approved. - Lifecycle. A locked skill whose version is
deprecatedorretiredgoes on, with askill lifecycle:note in the run log. Aquarantinedversion stops the run unless an administrator allowed this run (scf skills allow-run). Arevokedversion always stops it. See "Skill lifecycle" in "Trust". - A mandatory skill that cannot be used stops the step with
skill selection is blocked:; pin it or changeskills.selection.include. - Older runs without a lock stay readable and nothing is checked for them. The agent cannot write the lock file.
Exclusive capabilities#
Some capabilities should have one provider in a run, for example one database access skill. List them in skills.selection.exclusive:
skills:
selection:
exclusive: [cosmos-db-access] # at most 200 slugs, no duplicates; default []
- What a run does. For each listed capability, a run offers, locks and loads only the canonical provider: the skill that the canonical rules pick (the same rules as Canonical skill per capability). A requested skill that loses is left out, and the lock log says
skill lock: <id> left out: not-canonical. Two providers of an exclusive capability are never in one lock. - What stops a step. A tie that the rules cannot settle, a mandatory skill (
skills.selection.include) or a dependency that is not the canonical provider, and an owner choice that is invalid or expired all stop the step before the agent starts. The text starts withskills not resolved:(the catalogue) orskill selection is blocked:(the lock). Pin the skill, or change the choice or the list, then run again. A tie on a high-risk capability (skills.unresolved.high_risk) always stops. - Owner overrides are in
skill-overrides.yamlin the data folder. They apply to all repositories, or to a list of them. An override can be given an expiry date; after it, the override is no longer used and the step stops. - Project pins are in
.claude-factory/skill-pins.yamlon the default branch of the repository, never on a work branch. A pin settles a tie for its capability. Check the pins withscf skills project-pins check, and the overrides withscf skills overrides check. - Empty list. With
exclusive: []no check runs and the run behaves as it did before. - Record. The decision is saved in
run.jsonassummary.skillSelection: for each capability, the outcome, the winner (id@version), who decided it, up to 5 rejected skills with their codes and levels, and a count of the rest. It holds ids, versions and codes only, no text.
Complementary providers#
Two skills of one exclusive capability can work together, for example a database skill and a skill for its migrations. A skill says so with composable in skill.yaml: a list of its exclusive capabilities, each one also in capabilities (at most 32 slugs, no duplicates, default []).
id: postgres-migrations
version: 1.0.0
capabilities: [postgresql, postgresql-migrations]
composable: [postgresql]
- When both load. A provider ranked after the winner of an exclusive capability loads with it only when both list that capability in
composable. A provider withoutcomposableis never loaded with the winner. A provider that loses another exclusive capability it also provides stays out, and so does one whose capability is not composable for both. The canonical selection marks itoverlap. - Order. The winner loads first, then the companions in rank order. Each loaded section has its own
foundry-skill id/version/digestattribution. - Budget. Companions are offered and requested like any other skill, but they come after every other requested skill. A companion that does not fit is left out with
over-budget(the resolver) oromitted(the payload). It never displaces the winner. - Record. The run record lists
companions(id@version) for each capability, next to the winner. - Older versions. Foundry versions before this change refuse a
skill.yamlwithcomposable, because the manifest is strict. Upgrade them before you publish such a skill. Built-in skills do not usecomposable, and their digests and pins do not change.
Skill modes in a flow#
A flow can say how it gets its skills: skills: {mode: planned} (the default, from the plan gate), skills: {mode: explicit, ids: [...]} (the flow names them) or skills: {mode: off}. A claude step can set skills: off to get no skill block. mode: off also skips the skills of skills.selection.include, and the log says "skills: off for this flow". Only administrators can save flows. See docs/FLOW_AUTHORING.md for the syntax.
Skills from the plan to the coding run#
When issue-plan posts a plan, the Foundry keeps its own record of the skill request on the machine that planned. issue-code-daily has a step plan_check after daily_branch. It reads the issue comments again and compares them with that record. If they match, the run uses the skills from the record and builds its skill lock from them at the first agent step. The skills are never taken from the comment text.
- Carried with a warning: if someone commented after the plan, a
path:evidence file changed since the plan's commit, or the technology of the repository changed, the run still carries the skills. It logs one warning for each change and makes the lock again. - Stops: if the plan comment was deleted or edited, a newer plan of ours has no record, the record was cleaned up, or the comments cannot be read, the run stops when skills are in play. The reason reads
skills not resolved: … plan the issue again. Plan the issue again, then resume; the check runs again. With no skills in play the run only warns and goes on. A stored record that is invalid always stops the run. - Older plans: if nothing is stored (plans made before this feature, on another machine, or run with a custom flow), the run codes without carried skills, as before. This includes the skills in
skills.selection.include. - The record lives on the planning machine. To carry skills, plan and code on the same Foundry. A record for the same issue number in another repository is never used.
- Retention: old records are cleaned up. A run that needs a cleaned-up record stops as above.
- Same skills as
issue-gitflow. For the same plan and commit,issue-plan+issue-code-dailylock the same skills (id@version, digests, load order) as oneissue-gitflowrun. A skill that cannot be used stops the coding run underskills.unresolved; resume checks again. - On the run page. The Skills card says in one sentence what the plan check found (record used, none found, cleaned up, comment edited or gone, newer plan, comments unreadable) and whether the run stopped. It never shows ids from the record or comment text.
Missing and conflicting skills#
After the plan gate (risk_gate in issue-gitflow, post_plan in issue-plan) the run checks the skills the plan asked for. A skill that cannot be used stops the run before the next step. The Foundry does not carry on with the generic coder without telling you.
skills:
unresolved:
unknown: stop # not installed (stop or warn, default stop)
missing: stop # a dependency is missing, too old or in a cycle
untrusted: stop # excluded, unapproved, unpinned, changed, unverified or wrong role
conflict: stop # conflicts with another skill, or the selection is blocked
oversized: stop # too large, or over the skill or token limit
high_risk: [migration, migrations, security, messaging, kafka, rabbitmq, amqp, queue, outbox]
- Default: every kind stops the run. The stop reason names each skill with a stable reason code (the codes under Decisions above) and says what to do, for example to pin or approve the skill.
- Warn: set a kind to
warnto let low-risk work go on. Each warning is saved inrun.json(skillPlan: the action, the selected skills and every unresolved skill) and written to the log as⚠ …. - High risk always stops, whatever the policy says. A skill is high risk when its id, category or a capability equals a
high_riskterm. Terms match whole hyphen-separated words, soinsecurity-notesis not high risk. A package that saysrisk: highis high risk too. Amediumskill also always stops; onlylowcan be warned about. A mandatory skill (skills.selection.include) that cannot be used also stops. - Resume: resuming a stopped run checks the skills again. Install, approve or pin the skill (see Pinned versions and integrity), then resume. If the stored request cannot be read again, the run stays stopped.
- Unchanged: a run whose plan asks for no skills, with no
skills.selection.include, is not checked. Older run files stay valid. - Where to see it: the stop reason and warnings are in
run.jsonand the live log. The run page shows the skills in a card; see Skills on the run page. The plan comment lists the required skills; stop reasons are not posted to GitHub.
Skills in Claude sessions#
A Claude session gets only the skills of the run's lock. The Foundry puts their text in one <foundry-skills> block at the start of the prompt, in front of the task. Nothing is written to the repository, so no skill file can appear in a diff or a commit.
- Only locked skills. The note every session gets says that the block is the only skill guidance to follow; the agent must not use, load or look for any other skill. A Claude session whose lock holds skills also runs without your personal Claude setup (skills, MCP servers, plugins, hooks and settings), even when
isolate_agentsis off. The log says so. Steps withskills: offorskills: catalogget no skill block, so they do not turn this on by themselves; reviewer steps always run isolated. - Rules win. The block says that the Foundry's safety rules and the instructions of the user and the task win over a skill, and that a skill cannot grant a tool, a permission or network access. Only the description and the instructions of a package are used; its tools, profile and files never reach the session. Text that looks like a block tag is escaped.
- Size. The block may not be larger than
skills.selection.max_tokens. A skill is added together with the skills it needs, or not at all. A skill that does not fit is left out and logged. If a mandatory skill does not fit, the step stops withskill selection is blocked: … does not fit the skill context budget; raisemax_tokens, then resume. - Recorded. The step record in
run.jsongetsskills:loaded(id@version, in load order),omitted(only when something was left out),bytesandestimatedTokens. Steps without a lock have noskills. The log shows the same sizes. - Repairs and fallbacks. The full text is given once per Claude session. A step with
resume:that continues the session of an earlier step (same agent, same locked block) gets only a one-line reminder with the skill ids, not the text again. A new session gets the complete block exactly once: a retry, a fallback to another model or provider, or a resume that cannot continue the old session. Before every session the lock is checked again; if the skills changed, the step stops. - Log.
skill context: reused …(nothing added),skill context: loaded …(first session),skill context: reloaded … new session(retry, fallback or fresh session) and! skill context: rejected (…)(lock check failed). The log shows ids, sizes and token estimates, never the package text. The step recordskillsalso getsstate,digest,attachedBytesandattachedEstimatedTokens;skills_digestis kept for the next resume. Older run files without them still work. - Codex sessions get the same block; see Skills in Codex and local-model sessions. Reviewer steps get review checks on both agents; see below.
Skills in Codex and local-model sessions#
A Codex step, also on a local model, gets the same <foundry-skills> block as a Claude step of the same run: after </instructions> and before the task. Nothing is written to the repository.
- Isolation. A run whose lock holds skills runs its Codex steps without your personal Codex setup, even with
isolate_agents: false. The mode depends on how the step signs in; see "Codex and your personal setup" in When something goes wrong. A step held by the OS sandbox gets the block and the record too, with no flag and no note. - Old Codex CLI. If the CLI has no
--ignore-user-config, a step with locked skills is refused before Codex starts (skill selection is blocked: this Codex CLI has no --ignore-user-config …). Update the CLI, or use a local model orCODEX_API_KEY. Without locked skills, the step runs as before. - Size and record. The same limits and the same
skillsrecord apply as for Claude. A mandatory skill that does not fitskills.selection.max_tokensstops the step before Codex starts. - Every session. Codex never reuses the block: a resumed session, a retry and a fallback each get the full text again. The log says "the full block is sent again".
- Review steps. A review step on Codex gets the review checks of the run's lock.
- No lock, no block. A run without a lock is unchanged.
Skills, tools and connectors#
A skill can say what it needs: a shell, network access, write access or a connector. The plan gate compares that with skills.tools and with what the flow gives.
| What the skill needs | Checked against | Code when it fails |
|---|---|---|
| Shell, network or write access | skills.tools (the policy) | tool-not-approved |
| Shell or network | What the flow gives its Claude steps | tool-not-in-flow |
| A connector | Nothing: no connector can be granted yet | connector-unavailable |
In this version the check decides whether the run goes on. With
missing: stop(the default) a skill that fails it stops the run, so nothing of it is used. Withmissing: degradethe run goes on with a warning, and Claude coder sessions leave the skill out (see below). Codex sessions and reviewer steps are not checked yet. No connector is granted in either case.
skills:
tools:
shell: true # a skill may ask for a shell
network: false # a skill may ask for network access
filesystem: write # read or write
missing: stop # stop or degrade
It decides whether a run may go on with a skill. It does not limit what a session can do.
- When it is checked: after the plan gate, at the start of an explicit flow, and again on resume. It applies to planned, explicit and
plan_checkruns. A run with no skills is not checked, and older run files stay valid. - "The flow has it": a Claude step gives a shell or network through its permission mode, its
allowed_tools(Bash…,WebFetch,WebSearch) or the sandbox. A flow with aflowstep is not checked fortool-not-in-flow. tool-not-approved: allow the tool inskills.tools, or plan without the skill; then resume.tool-not-in-flow: give a Claude step the tool, or use another flow; then start again.connector-unavailable: no connector can be granted in this version. Remove the skill from the plan, or take it offskills.selection.include.degrade:- The run goes on with a
⚠warning. - The skill, the skills that depend on it and the dependencies only it needed are listed in
skillPlan.tools.leftOut. - A mandatory skill still stops the run, and so does a skill that a mandatory skill needs, directly or through other skills.
- "Left out" is a record and a warning at the plan gate. Claude coder sessions then leave the skill out; Codex sessions and reviewer steps are not checked yet. Use
stopwhen the skill must not be used at all.
- The run goes on with a
- Where to see it: the stop reason,
run.json→skillPlan.tools(action,met,missing,leftOut,warnings), and the log line· skill tools: <key> <need> — <sources>.
In a Claude session. Before every Claude coder session the locked skills are checked again against skills.tools and against what that step gives.
- Loaded only where it fits. A skill that needs a shell is loaded only into a step that has one. In a step without it (for example a read-only step) the skill is not loaded and the step is not refused. The log says
· skill tools: <key> not loaded in this step [tool-not-in-step]. - Policy changed after the lock. The session is refused with
skill tools: <key> [tool-not-approved]: …. Allow it underskills.tools, or setskills.tools.missing: degrade, then resume the run. Withdegradethe skill is left out and the log says! skill tools: <key> left out [tool-not-approved]. A skill that depends on it is left out too. - Isolation. A session stays without your personal Claude setup when a skill was left out. A step with
resume:starts a new session instead of continuing one that may hold the skill. - What the run records. The step record has
skills.grant: the permission mode, the approved tool rules, the sandbox, the capabilities (shell, network, files) with where each came from,connectors: [], what was met and what was left out. The log has one line with the same counts. - It is an estimate. It is built from the step's settings and decides only which skills are loaded. It does not limit the session. The repository's own
.claude/settings.jsoncan approve more tools than the record lists. allowed-toolsin SKILL.md is never granted. Tools come only from the step'sallowed_toolsand the permission mode.- Codex sessions and reviewer steps are not checked this way.
Skills on the run page#
The run page has a Skills card; users see it on their run page too. It has two lists.
- Requested: what the plan asked for, and what the administrator always includes. Each item has a state: Selected, Missing (not installed, or a dependency is unavailable), Conflicting, Not approved, Too large or Not checked (no answer yet). The reason, the evidence and the message of a problem are shown when there are any.
- Resolved: the skills the run uses, in load order, with their dependencies. Each row shows version, category, why it was chosen, about how many tokens of context it needs, its integrity, and how the session got it: Loaded, Reloaded, Reused or Left out (over budget). Reviewer steps show Review checks given.
Integrity words: Verified (the package matches the lock), Changed, Missing, No longer pinned, Not approved, Cannot be verified, Not checked and Not locked yet (the run has not reached its first agent step).
- Up to a minute old. Integrity and category come from the skill list the server keeps for one minute.
- Evidence. A path is shown as text. An issue is a link when the repository is
owner/name. - Status line. The card says when the lock is missing or was changed, when the run planned again (new skills are locked at the next agent step), when the run stopped because a skill cannot be used, and the context estimate.
- Why these skills. A short list at the end of the card says why each skill was chosen. It has one line per capability the run involves: the winner and the reason it won (for example
trust tier), the companions that load with it (composable), and up to 5 rejected skills with their reason (for examplenot ... (outranked: evaluation)). Each group of identical copies names the copy that was kept and the copies that were not offered. Project pins and owner overrides that the run applied are listed with their target. The list is capped at 20 capabilities and 20 groups of copies, and it says how many more there are. It only names skills the run involves, so the full catalogue is never shown. The list is hidden when there is nothing to explain. - Admins also see the source (built-in or administrator folder), the digest, who set a pin or override, and the commit of the project pins. Users never see digests, sources, authors (
by), commits or folder paths. - Older runs without skill information show no card. Older runs without the "Why these skills" data show the card without that list.
The same data is the skillView field of GET /api/runs/:id and of the update events of the run stream. The run list does not carry it.
Review checks for reviewers#
A skill package may hold an optional REVIEW.md next to SKILL.md: a few short checks for a reviewer (for example a Kafka skill checks delivery and ordering; a database skill checks the data model and migration risks). It is at most 8 KiB, must not be empty, and needs the reviewer role in skill.yaml (or no roles). It is part of the package digest, so a change needs a new pin.
A flow step gets these checks with skill_role: reviewer on a claude step. The plan_review step of issue-gitflow and issue-plan, and the review_N steps of issue-gitflow and issue-code-daily, have it. Without skill_role a step is a coder, as before.
- Compact. The block (
<foundry-skills role="reviewer">) holds only the description andREVIEW.mdof each skill, neverSKILL.md, references, scripts or other files. It is limited byskills.review.max_tokens(100–20000, default 3000) andskills.review.max_skill_tokens(50–5000, default 1000). It is always smaller than the coding block of the same run and never aboveskills.selection.max_tokens. A skill over its limit is left out and logged. - Only coding skills. Only skills that were loaded for coding and allow the
reviewerrole are used. Skills that were not selected do not appear. Code review uses the run's skill lock. Plan review, before any lock exists, uses the skills of the draft plan'sSKILL_REQUESTif they resolve; otherwise the reviewer gets none and the log says why. - Read-only. A reviewer step must be read-only (
permission_mode: plan, ordontAskwithout Edit, Write or Bash) and may allow onlyRead,Glob,GrepandLS. It cannotresumeanother step's session. A flow that breaks this is refused when it is loaded, and the step refuses to run for an older stored flow. A reviewer Claude session runs without your personal Claude setup. A Codex reviewer runs read-only, ignores your Codex user config and has MCP servers and hooks switched off. - Same on both agents. The block is the same for Claude and Codex. The step record gets
skillswithrole: "reviewer".