User guide: contents

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 (an https:// 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, verified or community), 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, weekly or monthly), update.reviewer (optional; a GitHub user or team, as owner; see below), owner (a GitHub user or team such as @octocat or @acme/skills) and review (see below). Unknown keys are refused.

  • Refs. ref.approved is 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 in ref.track, which names what to watch for updates. A cadence other than manual needs ref.track.

  • Status. enabled, disabled, quarantined or retired. Only enabled sources may be used. An enabled source needs ref.approved and has no reason; every other status needs a reason.

  • Review of a community source. review records who checked the maintainer and the licence, and when. It has four fields: by (a GitHub user or team, as owner), on (YYYY-MM-DD, not in the future), commit (the full commit id, equal to ref.approved) and licence (one of licence.allow). An enabled trust: community source is refused without review, and when review.commit is not ref.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-skills is on it, so a source with that repository must say trust: community; any other trust is refused and the message names trust.

  • Source profile. An optional profile says 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 without profile is unchanged. scf skills sources check prints provider confluent (1 portable) for the source. It refuses a mapping to an invalid slug, a duplicate key and a profile without provider. 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.reviewer names the GitHub user or team that reviews a new commit of an enabled trust: community source when scf skills update --due runs without a person. The tool never invents a reviewer. Without it, such a source prints PROBLEM … needs update.reviewer and no pull request is opened. The field is part of this file, so it is set by a reviewed pull request. scf skills sources check validates 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 id is and however the address is spelled. scf skills connectors lists them offline; it reads no files. The connectors are awesome-copilot (https://github.com/github/awesome-copilot): paths must be skills or a folder below it, adapter: agent-skills, trust: community, and licence.allow only MIT; and anthropic-skills (https://github.com/anthropics/skills): paths must be skills or a folder below it, adapter: agent-skills, trust: verified, and licence.allow only Apache-2.0, with a matching licence.reviewed (see Licence review and provider assumptions); and vercel-agent-skills (https://github.com/vercel-labs/agent-skills): paths must be skills or agent-skills or a folder below one of them, adapter: agent-skills, trust: verified, and licence.allow only MIT (see Vercel agent skills). Any other value is a PROBLEM and exits 1. A connector ships without a commit, so ref.approved is still needed on each enabled entry.

  • Check. scf skills sources check [<file>] reads skill-sources.yaml in the current folder, or the file you name. It works offline and does not need the data folder. It lists each source and then OK <n> sources, <m> enabled, and exits 0. A reviewed source is listed with reviewed by <by> on <on>. Otherwise it prints one PROBLEM <where>: <message> line per error and exits 1. Credential-like values are never printed.

  • Import and scan. scf skills import and scf skills scan read the same file. A community source without a current review is refused as an invalid source list: a PROBLEM line 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 in skill-sources.yaml) and skills (1 to 100 entries). Each skill has a name (a slug, unique in its source), class (portable or vendor), capabilities (1 to 32 slugs), high_risk (0 to 32 capabilities, each one of the skill's capabilities), requires and evaluations (0 to 16 slugs). exclusive (0 to 200 slugs) lists the capabilities that allow only one winner. Unknown keys are refused.
  • requires. config and live_services hold names only: a slug or an environment variable name such as DATABASE_URL, never a value. tools is one or more of shell, network and write. connectors is a list of slugs.
  • Rules. high_risk is a subset of capabilities. A capability belongs to either vendor or portable skills, not both, so use separate capability ids for each class.
  • Check. scf skills profiles check [<file>] reads skill-source-profiles.yaml in 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, then OK <n> skills in <m> sources, <k> exclusive, and exits 0. Otherwise it prints one PROBLEM <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 in skill-source-profiles.yaml.
    • licence: the stored licence evidence must be allowed. 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 as scf skills scan.
    • requirements: every tool or connector the package needs must be in the skill's requires. An undeclared need blocks.
    • evaluation: each kind in the skill's evaluations needs 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 PROBLEM line is printed or the canonical recommendation has a CONFLICT (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. skill and kind are slugs, digest is the package digest, verdict is passed or failed, and owner is a GitHub user or team. reviewed and expires are dates. report is optional and may hold the digest of a scored record. Up to 500 records.
  • Lifetime. expires is at most 365 days after reviewed, and not before it. reviewed may 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 failed record at that digest blocks even when a passed one 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 exclusive capability that a skill of this source declares in skill-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 as discovery-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_risk is marked high-risk on its lines.
  • Lines.
    • CANONICAL <capability>: <id@version> <origin> (<level>) names the winner, its origin (staged, builtin or admin) and the level that decided it. only-candidate means nothing else competed.
    • CONFLICT <capability>: <rule>: <message> means the choice cannot be decided, for example tie high-risk when two skills tie on a high-risk capability.
    • NONE <capability>: no skill can be used means every provider is rejected.
    • REJECTED <capability> <id@version> <origin>: <code> gives each loser its code, then (<level>) when a level decided it, then gates=… 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 a PROBLEM line. A CANONICAL, NONE or REJECTED line 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 compare and 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.approved is 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 postgresql is exclusive. When the portable skill and another skill provide it, scf skills profiles evaluate supabase-agent-skills recommends one of them. The Supabase capabilities are not exclusive and get no CANONICAL line.
  • High risk. The auth capability (supabase-auth) and the row-level-security capabilities (supabase-rls-policies and postgresql-row-level-security) are in high_risk, so they are marked high-risk.
  • Evaluation. The portable skill needs a passed database-correctness record for its exact digest in skill-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_URL and SUPABASE_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-skills and then scf 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 an all override 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.json and files/<upstream path>. The licence and notice files of the same commit are kept with them: licence.json and licence/<upstream path>.
  • import.json. schema, source, repository, repositoryKey, commit, paths, adapter, digest, skills[] and rejected[]. An import also has retrievedAt (the UTC time of the fetch) and licenceFiles[]: each file's path with sha256 and bytes, or with a problem when it cannot be read or staged. The list is the same as in licence.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, NOTICE and similar file of the staged skill roots is copied byte for byte into licence/, with its path, size and SHA-256 in licence.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 a PROBLEM.
  • 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 scan works 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 ~/.netrc are 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 a REJECTED line 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 is conflict: 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 PROBLEM is 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.json exactly: a changed file, a link or an extra file is refused with a PROBLEM line and nothing is scanned. Then every skill is scanned with the static scanner (#472), and the exceptions are applied.
  • Lines. One FINDING line per finding: its stable id, severity, disposition (open, accepted, expired or review-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, or BLOCKED <skill>: <n> high findings. Each REJECTED line 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 low finding prompt-injection.quoted-example and 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"). The FINDING line gives the path and line. Anything else (an instruction, no quotes, no cue, a cue in another sentence) stays high. Credential, policy and network rules have no such exception.
  • Web addresses. One ADDRESS <skill> <mentioned|called> <path>: <address> (<why>) line per web address (http:// or https://) in the instructions and in the files of the skill. mentioned means the agent only reads the file: Markdown, data, metadata.json, anything under references/ or evals/. called means the file is code: anything under scripts/, a file with a code extension (.sh, .py, .js, .ts, .html, .ps1 and similar), a file without an extension, or a file that starts with #!. A mentioned address never blocks. A called address blocks the skill as holds 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 check prints 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 declares mcp_tools prints EXCLUDED <skill>: <reasons> instead of OK. 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 ] or test -f .env is not an upload and raises no finding. A real upload of a credential-like file is still high: curl -F "f=@.env", curl -T .env, curl --data-binary @.env, scp .env … or nc … < .env.
  • Exit codes. 0 when no skill is blocked. 1 when a skill is blocked or excluded, or a PROBLEM is printed. A PROBLEM names 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 without ref.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 … unmapped names a skill the profile does not know or whose companion skill is missing; CONNECTOR … absent names a profile skill that is not in the import. The report reads nothing else and runs nothing. reviewed=no means 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), or review: required when the finding must be looked at again. An entry whose date has passed is expired, and review: required is review-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 prints PROBLEM no workflow profile for … and exits 1. The profile is in src/skills/workflow-rules.ts and 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 with not-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: off or without such a step is flow-incompatible; a skill with no compatible flow is no-compatible-flow.
  • Lines. CONFLICT <code> <effect> <skill> <path>[:<line>]: <evidence> for each finding. The effect is exclude (the skill is left out) or report (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> more says 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 is EXCLUDED <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 without ref.approved, an invalid source list, or no workflow profile).
  • Not done. Nothing promotes a candidate yet. workflowBlockers, declaredPackage and workflowProvenance in src/skills/workflow.ts are for a later promotion step to call. A promoted skill would record its declaration in SkillProvenance.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 example spring-boot with transactions maps to spring-boot-transactions, and java maps to java-language. The mapping is used for this comparison only: the skill, its digest and its package are not changed.
  • Skills. One SKILL line for each imported skill, sorted by id. Its tier is vetted-community when the source is enabled and reviewed at the approved commit (review.commit equals ref.approved), and discovery-only otherwise. A skill with no mapped capability also prints NO-OVERLAP <id>.
  • Excluded. EXCLUDED <id> stale means a declared version is no longer supported, and EXCLUDED <id> incompatible or compatibility-unchecked means the skill cannot be checked against its declared versions. An excluded skill is never recommended. A skill that normalization rejects prints EXCLUDED <folder> not-normalized.
  • Collisions. COLLISION lists each collision with its class, its reason and the members it names. blocking at 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. RECOMMEND names the winner and the level that decided it (for example trust-tier, owner-override or only-candidate). The built-in skill wins on trust tier over a vetted-community skill, unless an owner override says otherwise. CONFLICT names the capability and the reason, for example that no skill is eligible.
  • Notes. NOTE gives 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 PROBLEM when 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: community source, each staged skill gets one line after the report: PROMOTABLE <id> when every check passes, or REFUSED <id>: <codes>. The codes are listed in the order below. A staged entry that did not normalize prints REFUSED <folder>: not-normalized. These lines come before COMPARED, which stays last.
    • source-not-enabled, review-missing, review-stale, import-not-approved: the source is not enabled, has no review, or its review or the staged commit does not match ref.approved.
    • licence-missing, licence-ambiguous, licence-rejected, licence-review: the licence evidence of the import. licence-missing means 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 in skill-scan-exceptions.yaml. compare runs 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: no kind: fixtures record for the skill at its exact digest, or the record has expired. fixtures-failed: the record at that digest is failed. See Evaluation records.
  • Inputs. For a trust: community source, a missing skill-scan-exceptions.yaml means no exceptions, and a missing skill-profile-evaluations.yaml means no records. An invalid file prints PROBLEM <file> <where>: <message>, no COMPARED line, and exits 1. Fixture results come only from the kind: fixtures records, matched on the skill id and the exact digest. The gate records of scf skills gate are not used, and a missing record is never read as failed.
  • Exit codes. 0 when there is no conflict, no blocking collision and no PROBLEM. 1 for a CONFLICT, a blocking collision, or a PROBLEM (no staged import, unknown source, a source without ref.approved, an invalid source list, a built-in skill that cannot be read, an invalid skill-overrides.yaml, or an invalid exception or record file). For a trust: community source, 1 also when a candidate is REFUSED, 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 use compare will 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 as oracle-database@19. A skill is compatible when its version range admits the version you give. Without any technology, no COMPATIBILITY verdict 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 the package.json files 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 as 18.2.0 is used as it is, ^18.2.0 gives 18 and ~18.2.0 gives 18.2. A range such as >=18, <18, latest or workspace:*, a missing version, or two different versions, give unknown, so the verdict is unchecked. 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 a PROBLEM and no verdict.
  • Source scope. A source entry in skill-sources.yaml can state what upstream does not: scope with up to 8 capabilities and up to 8 technologies (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. ACCESS lists 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. LICENCE is per skill, from the source's licence.allow. A skill whose licence is not allowed is REFUSED. The attribution files of an allowed skill are listed as NOTICE, 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. RECOMMEND names the winner and why it won; CONFLICT means no winner can be chosen; NONE means 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 incompatible or unchecked, 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. A PROBLEM line 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) prints PROBLEM and 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. frontend group: skills/react-best-practices, skills/composition-patterns, skills/react-native-skills. typescript group: none yet. skills/web-design-guidelines is listed but on hold, so it is never offered. skills/vercel-deploy-claimable is not selected: the report says NOT-SELECTED skills/vercel-deploy-claimable and it is not evaluated, so its licence, findings and size do not change the verdict (its bytes still count toward the import limits while paths is skills). The profile's capabilities, technologies and category replace the ones the upstream SKILL.md declares.
  • A declaration becomes reviewed only when its digest (the package digest of that folder, skill-relative paths) is in that folder's reviewed list in frontend-rules.ts. A list entry is added by a code change, after someone has read that exact SKILL.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 evaluation skill-gate to have passed for its digest. No result can be recorded yet, so evaluation missing is shown for every folder and the verdict is blocked. A failed result also blocks.
  • Why web-design-guidelines is 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 CONNECTOR line 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 example missing, ambiguous, review or rejected), blocked when a high finding is not accepted, or not-normalized when the normalization step rejects the skill, or evaluation (<suite>: <codes>) when a suite of skill-evaluations.yaml is not passed for the digest (Confluent skills). An EVALUATION line per suite follows the other lines. An EFFECT <id> <digest>: <verdict> line per skill follows, with the effect code in brackets when the rule refuses it, or with accepted by <owner> on <date> when a decision was used. A path whose effect is refused prints REFUSED <path>: effect (<code>), with the codes of Score and gate an import. The ADDRESS lines of Scan an import follow them, then one CHECKED line 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 placeholder Copyright {yyyy} {name of copyright owner} or Copyright [yyyy] [name of copyright owner] or a filled-in owner, and with http or https addresses. A notice that adds a restriction, and any other changed word, gives ambiguous … the licence is not recognised.
  • Exit codes. 0 when every path is promotable. 1 when a path is refused or a PROBLEM is printed (for example a missing import, an unknown source, a source that is not enabled, an invalid exception file, or an invalid skill-evaluations.yaml).
  • Manifest fields. The promotion manifest (schema 1) lists the provenance of each promoted package. Two optional fields record who vouches for it: trust is the trust tier of the source (for example community) and is always set; review holds by, on and commit of 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, and attributionProblem works 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 its license: line in SKILL.md).
  • How to review. Read the lines promote check printed above the LICENCES line, then set licence.reviewed to the printed digest in skill-sources.yaml, by pull request. The state not reviewed means the field is missing; changed since the review means it is an older digest.
  • Refusals. Until the digest matches, every path of the source is refused with status review, and promote check exits 1.
  • Example entry. Placeholders are shown in angle brackets; use the commit you reviewed and the digest that promote check printed 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, its compatibility and its instructions:
    • claude-tools: a Claude tool name, such as WebFetch, or Read next 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-tools field 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, name or default); the licence verdict (allowed, review, rejected, missing or ambiguous) 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 as same name, even when the content differs (for example an upstream java against the built-in java).
  • 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 REVIEW line, then per category CATEGORY, SKILL, LICENCE, ATTRIBUTION, NOTICE, SCAN, NAME and COLLISION lines, PROBLEM lines and a closing REVIEWED line that says nothing was promoted. The same input gives the same bytes. Credential-like text shows as (hidden).
  • Markdown. --markdown prints 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 PROBLEM and checks no collisions.
  • Exit codes. 1 when a skill is blocked, a licence is not allowed, a blocking collision exists or a PROBLEM is 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.

  1. Review. A person checks the maintainer, the licence and the commit to approve.

  2. Record the review in skill-sources.yaml. Set status: enabled, ref.approved to the reviewed commit and licence.allow to the licences it accepts. Add review with by, on, commit (the same commit) and licence (one of licence.allow). Start from the example in Skill library sources.

  3. Check the file. scf skills sources check must print OK and exit 0.

  4. Import. scf skills import spring-agent-skills fetches the commit into quarantine and keeps its licence files, which compare needs.

  5. Scan. scf skills scan spring-agent-skills. Each reviewed high finding is accepted by its id and digest in skill-scan-exceptions.yaml.

  6. Record the fixture results. Add one record per candidate skill to skill-profile-evaluations.yaml. Take the id and the digest from scf 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-14

    A record is for one digest, so a new upstream version needs a new record. The other fields follow Evaluation records.

  7. Compare. scf skills compare spring-agent-skills prints a PROMOTABLE or REFUSED line for each candidate. It exits 0 only when every candidate is PROMOTABLE. 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, quarantined and retired are refused. A connector with no entry yet (for example awesome-copilot) needs --owner: the first run adds its entry, with the commit, the paths and the licence from the connector. --owner is 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. Without ref.track, the commit is HEAD.
  • Reviewer. An enabled community source needs --reviewer <@user> for a new commit: the entry gets a review (by, on today, commit and licence). --licence <id> names the licence when licence.allow has more than one and there is no earlier review. A run that prints UNCHANGED needs no reviewer.
  • Where the change goes. The branch is skill-update/<source-id>-<commit> (12 characters of the commit). Only ref.approved and review of that entry change, or one new entry at the end of sources:. Every other byte of the file stays. The edited file must pass the same rules as scf skills sources check, or nothing is pushed.
  • Pull request. The title is Skill source <id>: update to <commit> (or first 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) or unchanged. 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 scan blocked flag 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 a CONFLICT is 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.
  • 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 whose skill-sources.yaml differs 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> unchanged line comes before the REVIEWED line when the old commit was compared, or CHANGES 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 gives OPEN, 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 or gh call, 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 on origin. When the branch cannot be removed, the PROBLEM names it: delete it by hand. A failed gh pr edit prints PROBLEM and 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.yaml on the default branch does not change.

Walk-through for the awesome-copilot connector:

  1. Propose. scf skills update awesome-copilot --owner @maintainer --reviewer @reviewer. The first run prints the pull request address and the REVIEWED line of the report.
  2. Review. Read the pull request: the changes since the approved commit, the licence (MIT for this connector), the attribution, the collisions and the scan in the body. Check that the reviewer named is the one who checked them.
  3. Merge. The source is then pinned by the merged file. scf skills import awesome-copilot fetches that commit, and scf skills review awesome-copilot shows the report again.
  4. Next update. When upstream moves, run scf skills update awesome-copilot --reviewer @reviewer again. While upstream is unchanged it prints UNCHANGED.

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. --due takes the enabled sources whose update.cadence is not manual and is due. A source is due when it was never checked, when its last check ended with PROBLEM (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. --all checks every enabled source and ignores the cadence. manual sources are never due by --due; they are checked only by scf skills update <source-id>. --due cannot be used with a source id, and takes no --owner, --reviewer or --licence: a community source names its reviewer in update.reviewer.
  • The list. The sources are read from skill-sources.yaml on origin/<default branch>, not from the working copy. A clone that is behind origin needs no git pull. A file that is not on origin, or that does not pass scf skills sources check, stops the run with one PROBLEM line per error.
  • Community sources. For a new commit of an enabled community source, the pull request names update.reviewer as 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 from licence.allow still needs a person: the source prints PROBLEM … needs --licence and 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, with not due and manual counts when some sources were skipped. The run exits 1 when any source printed PROBLEM, 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.json in the data folder (schema 1, written atomically). A missing or unreadable file means every source counts as never checked; the run prints one NOTE for 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 &amp;&amp; /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 folder the 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 .claude or .codex folder (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 when every has passed since the last one. It accepts <n>m or <n>h from 5m to 24h, for example 30m or 6h. 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 example skill-sources: awesome-copilot: OPEN <url> …. Secrets are removed from the lines.
  • Problems. A PROBLEM outcome becomes a finding of the detector skill-source-check on 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. gh must 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.yaml in the current folder. The gate checks each record against the suite it reads, so a record made with another suite is FAILED (suite-mismatch). A missing or invalid suite is a PROBLEM. Run score again after you change the suite.

  • TypeScript and Node.js suite. evals/skill-gate/typescript-node/suite.yaml scores these skills offline (selection, conflicts, context and tools; scf skills effect uses its instructions section, 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 is typescript or nodejs, 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.yaml scores 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 is testing, 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. Its instructions section has five scenarios for scf skills effect (see Measure the effect of a skill); each runs offline and needs javac and java for 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 mode 0600 in 0700 folders. The newest record is the one written last.

  • Catalogue description. When a skill has a catalogue_description in Skill selections, score judges its context dimension (the length check, description-cut and description-over-limit) on that text instead of the package's own. Its selection dimension 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. gate and scf skills release publish refuse with description-changed when the description is no longer the one scored, so run score again 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), or duplicate (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 example record-invalid, digest-mismatch, suite-mismatch, safety-blocked, licence-not-allowed, below-threshold, baseline-missing, regression, description-changed or an effect code (see Effect below); a dimension name follows the code where it applies. PROBLEM names the file or field that stops the command. GATED gives 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. Run scf skills score <source-id> to make the record.

  • Effect. gate also 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 check applies the same rule. Each verdict is:

    • helps: nothing is added.
    • harms: always FAILED with effect-harms. A decision cannot override it.
    • no-measurable-effect: FAILED with effect-no-effect, unless a decision with accept: no-effect exists for the digest.
    • No record: FAILED with effect-not-measured, unless a decision with accept: not-measured exists for the digest.
    • A record that cannot be read or is invalid: always FAILED with effect-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-level effect_decisions list (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 a PROBLEM line. 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's PASSED or FAILED line. An invalid skill-evaluations.yaml prints PROBLEM lines 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. score exits 1 when a PROBLEM is printed. gate exits 0 only when every skill passes and no PROBLEM is 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 PROBLEM for gate.

  • 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 offered cases use tasks on fixtures/orders-service, a small Spring Boot service with a JPA repository, tests and a Maven pom.xml. Two not-offered cases use fixtures/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 (a pom.xml or build.gradle with .java sources) and spring-boot (a dependency named org.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 only java, 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-service compiles with the plain JDK against the stand-in types in its src/stubs/java folder, so its tests run without Maven. cache-library is a Gradle project with a plain main test that compiles with javac, so Gradle is not needed to run it. web-tool runs with node --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 use cache-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
ScenarioFixtureAreaWhat the check decides
cancel-rulesorders-serviceSpringOrderService.cancel throws IllegalStateException for a PAID order and leaves it unchanged; throws NoSuchElementException for an unknown id; cancels and saves an OPEN order.
optimistic-lockingorders-serviceJPAOrder has exactly one @Version field, of type long, Long, int or Integer, and is still an @Entity with an @Id.
single-loadcache-libraryConcurrencyLoadingCache.get calls the loader once per key, also when two threads ask at the same time, and gives both the loaded value.
log-load-failurecache-libraryjava.util.loggingWhen 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, with repeat: 4 in 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 two orders-service scenarios, so 2 × 4 × 2 = 16 sessions.
  • JDK requirement. Each check compiles and runs Java with javac and java. Put a JDK 17 or later on the PATH of the terminal that starts the Foundry, installed outside your home folder, for example under /Library/Java/JavaVirtualMachines or 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 with javac -version and java -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.ts checks 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 the cache-library test passes with the plain JDK. It is skipped with a warning when javac is 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 a PROBLEM at instructions.scenarios (<id>) repo. The same limits as the other fixtures apply.
  • Digest. A suite without instructions keeps its digest. With it, the digest covers each scenario, mandatory and repeat, 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 an instructions score is FAILED (dimension-missing:instructions). The minimum number of samples is repeat × the number of scenarios. With mandatory: false the dimension is ignored when it is missing.
  • Paired baselines. A record may hold an agent block 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 is baseline-missing.
  • Refused records. A record whose agent block does not match its dimensions.instructions, names an unknown scenario, or repeats a run id is record-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 of gateChecks (at most 4). A candidate with two scenarios, repeat: 3 and 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 agent block, 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, prints SKIPPED <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_SANDBOX or FACTORY_USER_SANDBOX is off. The suite has no instructions. 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-failed or skill-not-loaded. A fixture step that fails, or a fixture that cannot be read, is a PROBLEM <id>: …; no instructions, and that skill gets no instructions score.
  • 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/ and skills.lock.json do not change. Every run has no GitHub token, an empty gh folder, no personal Claude or MCP settings, a short environment, and a private CLAUDE_CONFIG_DIR and HOME. 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, Grep and LS always; Edit, Write and MultiEdit for filesystem: write; Bash for shell: true; WebFetch and WebSearch for network: 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 NOTE line, and the record says so in agent.network (not-host-limited) and agent.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 interrupted and writes no record. Run it again to get a record.
  • evaluate and score. scf skills evaluate … --agent is refused with a message that points to score --agent. Plain scf skills score and scf skills gate are 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 runs repeat times in each arm (the instructions.repeat of 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:

    • helps when p ≤ 0.05 and the with-arm pass rate is at least 0.2 higher (pass-rate-higher);
    • harms when p ≤ 0.05 and it is at least 0.2 lower (pass-rate-lower);
    • no-measurable-effect otherwise (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 is never-selected as well.

  • Tokens and time. They are reported only. They do not change the verdict.

  • Output. One RUN line per run, one SKIPPED line per scenario that is not offered, then one EFFECT line. 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.json

    A refusal or an interrupt prints PROBLEM … (and INTERRUPTED on 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 the effect column of scf skills. A helps verdict passes. harms always blocks promotion. no-measurable-effect and a missing record need an owner decision (effect_decisions in skill-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 in evals/skill-effect/kafka/, all 5 scenarios are offered, so 5 × 4 × 2 = 40 sessions.

  • Example set. evals/skill-effect/kafka/suite.yaml has five Kafka tasks (idempotent producer, commit after processing, key by order, dead letter, retry with backoff) on the fixtures/orders project. 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.yaml has 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) and await-notifications (a bounded async send, in JavaScript). Four run on fixtures/ts-service and one on fixtures/js-package. It has no network and no credentials; each check is an offline node script with stubs. The cost is 5 × 4 × 2 = 40 sessions. The checks need Node 22.18 or later, for type stripping of .ts files.

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.
  • name and version: the name in SKILL.md and its version. An undeclared version is 0.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.yaml at the top of the skill, and .DS_Store files 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 .md and .txt files outside scripts/ and assets/: CRLF or LF line endings, and a byte-order mark.
  • In SKILL.md frontmatter: 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, including LICENSE.txt, REVIEW.md and evals/. 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.md whose 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 id and upstream. It matches the original only by fingerprint.
  • A changed name in SKILL.md: this is a different skill, not a new version.
  • A changed source id in skill-sources.yaml: all of its skills get new ids.
  • Neither: a renamed folder, or a changed paths entry.

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.

TechnologySupported lines
Java17, 21, 25
Spring Boot4.0, 4.1
Spring Framework7.0
Maven3.9, 4.0
Gradle8.14, 9
  • Where the version is read from. Java: java.version, maven.compiler.release or maven.compiler.source in pom.xml, and JavaLanguageVersion or sourceCompatibility in build.gradle / build.gradle.kts. Spring Boot: the Maven parent or plugin, the Gradle plugin org.springframework.boot, or a Boot BOM. Spring Framework: its org.springframework: dependencies. Maven and Gradle: distributionUrl in the wrapper files. A value such as latest, 3.+ or ^3.2 gives no version.
  • Stale. A skill is stale when its declared range fits none of the supported lines (for example spring-boot: ^2, or java: 8). A stale skill is never chosen: it is incompatible and an import of it is marked stale. A range that cannot be read is unchecked and is not chosen either.
  • Aliases. Other names are read as the same technology: jdk and openjdk are Java, springboot is Spring Boot, spring and spring-core are Spring Framework, mvn and apache-maven are Maven, and gradlew and gradle-wrapper are 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.

ClassReasonWhen
exact-mirrormirror.same-artifactOne repository, the same version and content, through more than one source record.
exact-mirrormirror.same-contentThe same content from another repository, or under another version.
exact-mirrormirror.settings-differThe same content, but the approved copies differ in skill.yaml settings (technologies, required tools, dependencies, provider, risk and so on).
version-reuseversion.reusedThe same skill and version with other content.
id-conflictid.other-versionThe same skill at another version, with other content.
id-conflictid.other-upstreamThe same id from another repository, with other content.
forkfork.declared-upstreamOther content, with a recorded link to the other repository.
forkfork.same-nameOther content under the same name in another repository.
capability-overlapcapability.sharedDifferent 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-conflict or mirror.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-candidate when nothing else competed). Each loser has a REJECTED <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 in skills.selection.include is not the canonical one) or winners-overlap (the best skill for one capability is not the best for another capability it also provides). NONE <capability>: no skill can be used means every candidate is rejected. NEW <capability>: <id@version> is a capability only the source declares. A skill with provider requirements prints its provider <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, or licence.reviewed is missing or stale), tools and 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. The OVERLAP line says how many capabilities were kept vendor-only. A capability that is portable for one staged skill and vendor-scoped for another is a PROBLEM.
  • Problems. PROBLEM lines 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 a PROBLEM. NEW, NONE and REJECTED lines 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 compare and 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 skills or repositories, 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 HEAD points at (git ls-remote, 20 s each). A repository git cannot read is UNRESOLVED and not stored.
  • Candidate list. <data folder>/skill-source-candidates.json, with status proposed, origin skills.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 as KNOWN and 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; then scf skills import fetches it. The revision shown is a proposal, not an approved commit.
  • List. scf skills discover with no operand prints the stored candidates and OK <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 PROBLEM or 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. source and skill name one skill of a source; source+skill is listed once. digest is the skill's digest in import.json. scope is portable (guidance for any provider) or provider (specific to one provider). provider is a lower-case slug, required for provider and refused for portable. product (name, version) and sdk (name, language, version) are required for provider; sdk.name is a package name such as @azure/cosmos. category and capabilities (1 to 32 slugs) describe the skill. owner is a GitHub user or team; reviewed is a date that is not in the future.
  • Catalogue description. catalogue_description is 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 to digest: 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, and scf skills score and gate judge the skill on that text (see Score and gate an import).
    • Where it reaches runs. scf skills release publish writes the text into the release entry of the skill as catalogueDescription (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 skills prints DESCRIPTION <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.
  • 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. requires lists what the agent itself must call or reach during a run: clis, mcp servers, credentials and services. 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> or service-<name> (at most 64 characters). scf skills selections check prints them sorted.
  • Check. scf skills selections check [<file>] reads skill-selections.yaml in 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), then OK <n> selections, and exits 0. Otherwise it prints one PROBLEM <where>: <message> line per error and exits 1. A missing file is a PROBLEM for this command (for readers, it means "no selections"). Credential-like values and control characters are never printed.
    • Overrides. An entry with catalogue_description also needs its source to be enabled with an approved commit in skill-sources.yaml, and the skill must be staged with this digest at that commit. Otherwise the check prints PROBLEM selections[<n>] (<source>/<skill>) catalogue_description: … and exits 1. The check reads skill-sources.yaml and the staged import only when an entry has an override; each line of an entry with one ends with description=<n> chars.
  • 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 overlap and scf skills promote check read 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.

  1. Copy the declaration. Copy the entry from docs/examples/skill-sources.confluent.yaml (also in the installed package) into skill-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.portable and the version ranges under scope and profile.products. The example was not checked against upstream;
    • set ref.approved to that full commit id, set status: enabled and remove reason.
  2. Run the checks. scf skills import confluent-agent-skills, then scf skills scan confluent-agent-skills, scf skills promote check confluent-agent-skills and scf skills overlap confluent-agent-skills.
  3. Give capabilities to a skill that declares none. Upstream skills may carry no capability metadata. For each such skill, add a skill-selections.yaml entry with its digest from import.json and its capabilities (see Skill selections).
  4. Write skill-evaluations.yaml from the EVALUATION lines of promote 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- or confluent- capability (including one the source scope adds) needs both suites. So does a skill without capabilities from a confluent provider source.
  • Fields. project is for compatibility only and is refused for messaging-semantics. note is 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 prints PROBLEM and 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.
  1. Read the result. promote check prints 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
  • passed means the suite passed for this digest. Otherwise the codes are listed, and the path is REFUSED <path>: evaluation (<suite>: <codes>) until they are gone. A messaging code ends with at <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, retries and dead-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) and unchecked (the project has no version for a declared technology).
  • Versions. overlap prints VERSIONS <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 example apache-kafka >=4.0, confluent-platform >=8.0, flink >=1.19.
  • Overlap. CANONICAL, CONFLICT, NEW and REJECTED lines 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 ELIGIBLE only when its licence verdict from the stored evidence is allowed, 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: a portable skill contains provider-specific text.
    • undeclared-requirement: provider-specific text needs a tool that requires does not declare.
    • provider-without-policy: a provider skill would replace a portable one 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 PROBLEM is printed or the usage is wrong. A PROBLEM names the file or field, and missing licence evidence asks you to import again. A missing selections file is a PROBLEM.
  • 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 PROBLEM and 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. schema must be 1. repository is github.com/<owner>/<repo>, the same form as source.repository in a release manifest. releases lists 1 to 200 entries. Each catalogue and version pair appears at most once. catalogue and each id are lower-case slugs of 1 to 64 characters; version is a version like 1.2.3. Each skills entry is {source, id}, and an id is listed once.
  • Review. review has by (a GitHub user or team, such as @octocat or @acme/skills) and on (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) and reason: 1 to 200 characters, trimmed, with no control character. A revoked id must be in skills, 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.yaml or skill-sources.yaml is 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> and REMOVED <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> or none.

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 of skills.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 in skill-releases/, or named by artifact on 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.autocrlf set, 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_description when 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 skills prints a DESCRIPTION line 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-boot skill needs a Spring Boot dependency in a build file, or sources that import org.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 the java skill, so java is loaded with it (and counts towards the limits).
  • TypeScript is not guessed from JavaScript. The built-in typescript skill needs TypeScript sources (plus tsconfig.json or a typescript dependency as extra evidence). Work that touches only JavaScript files does not get it, unless the issue names TypeScript or tsconfig.
  • Node.js is not guessed from TypeScript or JavaScript. The built-in nodejs skill needs evidence of the Node.js runtime in the module the work touches: engines.node, @types/node or a server package (such as express or fastify) in package.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 kafka skill needs a Kafka client or schema registry dependency, a Kafka import, or a kafka*.properties or kafka*.yaml file 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 oracle skill needs a sign of Oracle in the repository: an Oracle driver in a build file, an oracle.jdbc import, PL/SQL package files (.pks, .pkb) or tnsnames.ora. Plain .sql files, 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 provider block (see Write a skill) has the evidence line needs provider: <name> (<products>; tools: <tools>), for example needs 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 include are always included and never dropped. The catalogue says how many skills were left out.
  • include is 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_gate in issue-gitflow, post_plan in issue-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-daily has 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 node in the image for the checking tool, as create-split does.

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 than max_skills, or more than max_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 an include or exclude entry is not a valid id or the list is too long, the whole selection is blocked: nothing is selected and the other skills are marked blocked.
  • include is not a pin. An included skill still needs an approved, pinned version. The config is refused if an include id is also in exclude or in catalogue.exclude, or if there are more include ids than max_skills.
  • Decisions: every requested, mandatory and added skill gets a decision with a reason code: mandatory, requested or dependency when selected; excluded, unknown, unapproved, unpinned, mismatch, unverified, role, too-large, dependency-cycle, dependency-unavailable, dependency-version, conflict, over-count, over-budget or blocked when 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 in skills.releases.approved. A run locked to its packages still resumes: the old packages are found by exact id@version and digest, and they are not chosen for new plans, where the newest approved packages are active. Removing the old release from skills.releases.approved makes the lock fail with skill 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 invalid skill-lock.json stops it the same way. To go on, restore the exact package and pin, or plan again, then resume. scf skills release list shows which runs lock a stored release, so you can see who depends on it before you change skills.releases.approved.
  • Lifecycle. A locked skill whose version is deprecated or retired goes on, with a skill lifecycle: note in the run log. A quarantined version stops the run unless an administrator allowed this run (scf skills allow-run). A revoked version 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 change skills.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 with skills not resolved: (the catalogue) or skill 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.yaml in 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.yaml on the default branch of the repository, never on a work branch. A pin settles a tie for its capability. Check the pins with scf skills project-pins check, and the overrides with scf 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.json as summary.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 without composable is 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 it overlap.
  • Order. The winner loads first, then the companions in rank order. Each loaded section has its own foundry-skill id/version/digest attribution.
  • 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) or omitted (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.yaml with composable, because the manifest is strict. Upgrade them before you publish such a skill. Built-in skills do not use composable, 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-daily lock the same skills (id@version, digests, load order) as one issue-gitflow run. A skill that cannot be used stops the coding run under skills.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 warn to let low-risk work go on. Each warning is saved in run.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_risk term. Terms match whole hyphen-separated words, so insecurity-notes is not high risk. A package that says risk: high is high risk too. A medium skill also always stops; only low can 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.json and 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_agents is off. The log says so. Steps with skills: off or skills: catalog get 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 with skill selection is blocked: … does not fit the skill context budget; raise max_tokens, then resume.
  • Recorded. The step record in run.json gets skills: loaded (id@version, in load order), omitted (only when something was left out), bytes and estimatedTokens. Steps without a lock have no skills. 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 record skills also gets state, digest, attachedBytes and attachedEstimatedTokens; skills_digest is 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 or CODEX_API_KEY. Without locked skills, the step runs as before.
  • Size and record. The same limits and the same skills record apply as for Claude. A mandatory skill that does not fit skills.selection.max_tokens stops 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 needsChecked againstCode when it fails
Shell, network or write accessskills.tools (the policy)tool-not-approved
Shell or networkWhat the flow gives its Claude stepstool-not-in-flow
A connectorNothing: no connector can be granted yetconnector-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. With missing: degrade the 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_check runs. 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 a flow step is not checked for tool-not-in-flow.
  • tool-not-approved: allow the tool in skills.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 off skills.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 stop when the skill must not be used at all.
  • 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 under skills.tools, or set skills.tools.missing: degrade, then resume the run. With degrade the 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.json can approve more tools than the record lists.
  • allowed-tools in SKILL.md is never granted. Tools come only from the step's allowed_tools and 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 example not ... (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 and REVIEW.md of each skill, never SKILL.md, references, scripts or other files. It is limited by skills.review.max_tokens (100–20000, default 3000) and skills.review.max_skill_tokens (50–5000, default 1000). It is always smaller than the coding block of the same run and never above skills.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 reviewer role 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's SKILL_REQUEST if they resolve; otherwise the reviewer gets none and the log says why.
  • Read-only. A reviewer step must be read-only (permission_mode: plan, or dontAsk without Edit, Write or Bash) and may allow only Read, Glob, Grep and LS. It cannot resume another 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 skills with role: "reviewer".

Improve this page on GitHub