Skills Marketplace
SessionFS treats reusable agent capability as a governed package. A team can inspect one immutable Agent Skill or Agent Plugin, see what changes across tools, then consume the same package through local installation, an authenticated feed, MCP instruction loading, or an organization assignment.
The canonical authoring formats remain Agent Skills and Agent Plugins 1.0. SessionFS adds source identity, content hashes, DLP scanning, compatibility plans, consent, policy, and session provenance; it does not introduce a competing skill language.
Before You Trust a Package
Section titled “Before You Trust a Package”Every Marketplace detail response and dashboard dossier includes:
- Immutable
version_idand SHA-256 content hash - Source type, locator, subdirectory, and pinned commit when supplied
- Public, personal, or organization visibility
- Verification state and declared license
- Per-tool instruction count and deliberate omissions
- Risk flags for scripts, MCP declarations, and executable extensions
Previewed instructions are untrusted text. Previewing does not install, approve, or execute anything.
Install Disclosure
Section titled “Install Disclosure”Before anything is installed, every surface shows the same disclosure card, computed from the package bytes (never from what the package claims about itself):
- Per tool: the exact files that land on disk (for example
.claude/skills/deploy/SKILL.mdor~/.codex/skills/...for user scope), and what is not installed for that tool. - What can run: scripts (with their interpreter),
SKILL.mdfrontmatter hooks (event and matcher), executable extensions, and MCP servers (local command or remote origin, plus the environment variable names they read). - Tool pre-approvals:
allowed-toolsgrants the skill gives itself. - Network endpoints referenced anywhere in the package.
- Shell commands the instructions tell the agent to run (from fenced shell blocks), with file and line.
- Scanner findings, and hidden or bidirectional Unicode and control characters, shown visibly as
⟦U+202E⟧with file, line and column.
The card appears in sfs skill inspect and sfs skill add, in the dashboard package view and on the project Skills tab next to every pending approval, in MCP get_marketplace_package (display only; MCP never grants consent), and at GET /api/v1/marketplace/versions/{id}/disclosure?tools=codex,claude-code&scope=project.
A local install of a package that contains the hidden-character classes the publish gate refuses (bidirectional overrides or zero-width characters in SKILL.md or scripts, Unicode tag characters, raw control characters) needs --allow-hidden-unicode.
Updates and Re-Consent
Section titled “Updates and Re-Consent”Each version has a capability digest: two versions with the same digest grant the same access. An update is classified by comparing the version you consented to with the new one:
| Update | What changed | Personal sfs skill update |
Org assignment / daemon |
|---|---|---|---|
| Text | Instructions or assets only | One confirmation (--yes works); the consent you gave carries forward |
Applied by the daemon |
| Code | Script, hook, MCP-config or executable-extension bytes, with no new access | The changed capability’s flag again (--allow-scripts / --allow-hooks / …); --yes alone is refused |
Held until approved |
| Widening | Any new access: a new capability, hook event or matcher, MCP server or changed MCP command, remote origin, allowed-tools grant, network endpoint, environment variable, persistence target, or a lost publisher verification |
Always stops: --approve-widening (or an interactive yes to the widening prompt), plus the capability flags; --yes never approves it |
Never auto-applied |
sfs skill rollback and sfs skill sync --locked apply the same rules to the version they replace (and a locked sync is all-or-nothing). Any change to MCP configuration counts as widening.
sfs skill diff <name> and GET /api/v1/marketplace/versions/{from}/diff/{to} show the files added, removed and changed (with bounded text diffs), the capability changes, new scanner and hidden-character findings, and the update class.
Devices never apply a widening or code update on their own: sfs skill pull and the daemon keep the installed version and report the update as held. A held org update is applied once an organization admin approves it: POST /api/v1/orgs/{org}/skills/assignments/{id}/advance with approve_widening (or approve_code_change) records the approved capability digest on the assignment, and devices apply only a version whose digest matches. GET /api/v1/orgs/{org}/skills/assignments/updates lists every assignment with a newer version and its update class (paginated: pass the returned next_cursor as cursor). Assigning another version of a package to a target that already has one is refused with 409 use_advance (the server keeps one active assignment per package and target), and approving a project install request for a newer version of a package already approved in that project is refused with 409 reapproval_required unless the request explicitly approves the widening or code change.
Approvals bind the approved capability digest to the install request (or assignment) at approval time; approvals made before this release never count as approving a specific digest, and an update from such a grant is treated as widening. A grant is refused with 503 while the package’s manifest cannot be computed. A device applies a widening update only for the tools of the grant that bound that exact digest. Re-approving a text update in the dashboard carries the capabilities confirmed for the previous version forward; a code update asks only for the changed capability. Advancing an assignment keeps the old one as superseded history.
Consent receipts record the capability digest and the access that was consented to. When consent carries forward to a text update, the new receipt names the receipt it was carried from, so the audit trail shows which bytes a person actually reviewed.
An Enterprise exception, letting an org auto-apply code updates from a publisher whose signed release workflow it trusts, is reserved for verified publishers and trusted publishing and is not active yet.
Four Consumption Modes
Section titled “Four Consumption Modes”1. Local Device Installation
Section titled “1. Local Device Installation”Use sfs skill when an engineer or CI runner controls the filesystem:
sfs skill inspect https://github.com/acme/agent-capabilities.git --pin <40-char-sha>sfs skill add https://github.com/acme/agent-capabilities.git \ --pin <40-char-sha> \ --for claude-code \ --for codexThe client computes a deterministic plan, checks collisions and path safety, writes through a transaction journal, and records the actual output hashes. SessionFS refuses unmanaged-file clobbering. Consent is an inert allowlist, not a denylist: any skill asset that is not plain documentation or data (.md, .txt, .json, .yaml, images, LICENSE, …) needs --allow-scripts; SKILL.md frontmatter hooks: or allowed-tools: needs --allow-hooks; a hooks.json or hooks/ directory inside a skill needs --allow-executable-extensions. --yes alone is refused for any of these, with the message naming the hook or tool grant. Local MCP activation is not silently enabled.
2. Authenticated Target Feed
Section titled “2. Authenticated Target Feed”GET /api/v1/marketplace/feeds/{tool} returns a target-aware portability plan with the immutable download path, target install roots, and omitted components.
This feed is a SessionFS authenticated download-and-translate feed, used by sfs skill. The response says so explicitly, and for tools with a native catalog it also links it:
{ "projection_type": "sessionfs-portability-plan", "native_marketplace_install": false, "delivery": "authenticated-download-and-translate", "native_catalog_url": "https://api.sessionfs.dev/api/v1/git/public/sessionfs.git"}Native Tool Catalogs
Section titled “Native Tool Catalogs”SessionFS also publishes approved packages as native plugin catalogs: read-only git repositories that each tool’s own plugin command can add. Publish once, and the package installs with Claude Code’s /plugin install, Copilot CLI’s copilot plugin install, the Codex Plugins Directory, a Cursor team marketplace or gemini extensions install.
sfs catalog add --for claude-code # prints: /plugin marketplace add <clone-url>sfs catalog add --for copilot --org <org> # an org catalog (Team+)| Catalog | Who | Contains |
|---|---|---|
Public (sessionfs) |
Every signed-in user | Moderated public listings from verified publishers with no open high or critical scan findings |
Personal (sessionfs-user-…) |
Starter and above | Versions you consented to install |
Organization (sessionfs-org-…) |
Team and above | Org-wide assignments and approved install requests |
A native install skips SessionFS’s own consent prompt (the tool performs it), so a catalog only ever holds versions its scope already approved, and the tool’s own trust prompt still applies. A block, yank or quarantine removes the entry the next time the catalog is fetched. Tools do not uninstall plugins that leave a catalog; sfs skill pull and device reconcile still remove blocked packages where sfs runs.
Each catalog repository carries .claude-plugin/marketplace.json (Claude Code), .github/plugin/marketplace.json (Copilot CLI), .agents/plugins/marketplace.json (Codex) and .cursor-plugin/marketplace.json (Cursor), one plugins/<namespace>--<name>/ directory per package, and SESSIONFS-CATALOG.json, which binds every plugin to its version id, content hash and projection hash. Commits are deterministic: the same approved set always produces the same commit.
Authentication: git sends your API key through the sfs catalog credential helper, which sfs catalog add configures for the SessionFS API host only, ahead of an empty entry that keeps other credential helpers (such as a keychain or store helper) from receiving the key. Keys never appear in URLs.
One package never breaks a catalog: a package that cannot be projected safely, or that does not fit the catalog’s size budget, is left out and listed with the reason in SESSIONFS-CATALOG.json, and if a rebuild fails the catalog keeps serving its last good commit, unless that commit still carries a version that has since been blocked, yanked, quarantined or had its grant revoked, in which case the catalog answers 503 until a rebuild succeeds.
Cursor team marketplaces import from GitHub, GitLab, Bitbucket or Azure DevOps, so mirror an org catalog to a repository you own with sfs catalog export --to <remote>. Gemini CLI installs one extension per repository and lists public GitHub repositories, so it gets per-plugin repositories from the public catalog only. Antigravity only lists its official marketplace: clone the catalog and run agy plugin install <clone>/plugins/<plugin>.
Works In
Section titled “Works In”Every listing shows a per-tool works in matrix (sfs skill works-in <id>, GET /api/v1/marketplace/versions/{id}/works-in, and MCP get_marketplace_package):
| Badge | Meaning |
|---|---|
verified |
The tool’s own installer accepted the package in the SessionFS verification harness (L1), with the tool version and date |
format-valid |
The generated catalog entry matches the tool’s documented format (L0, checked in-process) |
regressed |
It verified on an earlier tool version and fails on the newest one |
expected |
Computed by SessionFS’s adapters; not yet verified |
The harness runs each tool’s installer in an isolated, short-lived sandbox with no secrets, as a separate unprivileged user from the harness process; it never runs a skill (that would need a model key, and SessionFS holds none). Works-in checks on private (org or personal) packages are a Team feature, with a daily per-organization quota.
3. MCP Runtime Instructions
Section titled “3. MCP Runtime Instructions”Agents can search and preview packages over MCP. To load instruction text into a live agent context:
- Call
request_skill_installwith the exactversion_id,content_hash, project, target tool, and scope. - A project owner/admin approves the exact hash from the project Skills tab in the dashboard (or
POST .../skill-install-requests/{id}/approvewithapproved_capabilities), confirming each elevated capability, or an organization admin creates a matching assignment. Approval is not available over MCP. If the approver is the same principal that filed the request, the server also requires a single-use confirmation code emailed to that account (POST .../skill-install-requests/{id}/challenge, 10-minute lifetime). - Call
load_marketplace_skillfor that project and tool.
The load response is instruction-only. It explicitly reports executable_components_included: false and untrusted: true with a fixed preamble marking the text as third-party data. MCP cannot write device paths or activate scripts, hooks, executable extensions, or local MCP servers.
4. Organization Assignments
Section titled “4. Organization Assignments”Team and Enterprise organizations can manage multiple registry references and assign exact package versions to the entire organization, one member, or one member’s device. Device assignments bind both the user and device identifier; another organization member cannot claim a guessed device ID.
Assignments are desired state. Clients still perform local preflight and installation. Enterprise policy may make an assignment required, but it does not turn the server into a remote code-execution path.
Supported Tools
Section titled “Supported Tools”sfs skill plans and installs packages for Claude Code, Codex, Gemini CLI, Cursor, GitHub Copilot, Amp, Cline, Kilo Code, and Roo Code. Roo Code support is legacy: its upstream repository was archived in May 2026, so the adapter keeps working but will not track future Roo changes.
Dashboard Workflow
Section titled “Dashboard Workflow”Open Marketplace in the dashboard to search everything visible to your identity. Search, the tool picker, sort and the category row stay on the page; the other filters sit behind Filters, and active filters show as removable chips. Each package card shows its version, installs and one capability label (Instructions only, or what else it includes). Opening a package shows what it can do, its source and where it was published from (Overview), the install disclosure card (Install details), the works-in and per-tool matrix (Compatibility) and the README. Choose Request for project to create an instruction-only request. Install in your tool at the top of the page shows each tool’s own commands for adding the SessionFS catalog. Project owners/admins decide pending requests from the project’s Skills tab, which shows the disclosure for the requested tools and, when the request updates a version already approved there, the changes to approve.
Organization members also see registry policy, each registry’s poll status (last and next poll, failures and the error summary) and effective assignments; owners and admins can Poll now. Organization owners and admins also get Updates to review: every assigned package with a newer version, its update diff and the full disclosure of the new version, with an acknowledgement (bound to the disclosure shown) and an Advance button. Private packages remain hidden from users outside the owning organization; unauthorized detail and download requests return 404 rather than disclosing existence.
Discovery and Freshness
Section titled “Discovery and Freshness”Browse by category (code review, testing, debugging, refactoring, documentation, security, frontend, backend and APIs, data, infrastructure, deployment, workflow, integrations, AI and ML, productivity) and narrow results by publisher, verified publisher, trusted-publishing provenance, declared license, worst open scan finding, capability (instruction-only, scripts, MCP servers, hooks, executable extensions, pre-approved tools), the tool a package is verified to work in, and how recently it was updated. The same filters are in the dashboard Filters menu, sfs skill search, the API (GET /api/v1/marketplace/packages) and the read-only MCP search_skill_marketplace tool. Every filter runs on the server before a page is cut.
The default ranking is fixed and documented, with no stars or reviews: packages that are not deprecated come first, then the text match (exact name, name prefix, name contains, description), then verified publishers, then how many tools the package is verified to work in, then a log-scale bucket of 30-day installs, then the newest version.
- README: the package’s
README.md(or the firstSKILL.mdbody) is shown on the listing. It is rendered without raw HTML, links are limited tohttp(s)andmailtoand carryrel="nofollow", remote images are never loaded (no tracking pixels), and images inside the package load only through the authenticated asset endpoint (PNG, JPEG, GIF and WebP checked by content; never SVG). Hidden and bidirectional characters are shown as⟦U+XXXX⟧and listed with their line and column. - Install counts by tool: installs over the last 30 days, per target tool. Counts under 10 show as
<10. The server keeps daily totals only (one count per account per version, tool and day) and service-key installs are counted separately. The CLI and daemon send these counts best effort;SFS_SKILL_USAGE_REPORTING=0or turning telemetry off stops them. - Freshness: last updated and last verified per tool. A package is stale when it has had no new version for 180 days and either its works-in check regressed or (for a verified GitHub publisher) its source repository is archived. Deprecated is set by the publisher, with an optional successor from the same publisher, which search and installs point to. Unmaintained means the publisher’s verification lapsed. A daily sweeper recomputes the flags.
Publishers get an analytics view (dashboard Publishers → Analytics, sfs skill publisher stats): installs by tool, version and day, active installs, uninstall rate, translation failures, update adoption, works-in regressions, scan findings and open reports. Every count is floored at 10 and the organizations installs come from are never shown. Personal publisher analytics need Starter or above, organization publishers the Team plan. Organization owners and admins on Team can see what their own members use (dashboard Marketplace → Organization usage, sfs skill usage --org).
Publishing Boundary
Section titled “Publishing Boundary”Publish with sfs skill publish, the authenticated API, or the publish dialog on the dashboard Marketplace page (for pre-built archives). Uploads are bounded, path-safe, DLP-scanned, instruction-scanned, normalized, and stored as immutable canonical archives.
| Who can publish | Visibility | Requirement |
|---|---|---|
| Any signed-in user on Starter or above | Personal (only you) | None beyond the upload checks |
| Organization owners and admins on Team or Enterprise | Organization | The package’s plugin.json must declare a license |
| Verified publishers enabled for public publishing by SessionFS | Public | A declared license; a publisher’s first public version, and any update that widens access, waits for review |
Browsing, downloading, and loading approved instructions are available to every signed-in user, including the Free tier. Any declared license value is accepted for organization packages, including UNLICENSED or proprietary for internal work; packages without one are rejected with license_required.
Hosted publishing limits:
- A package can be at most 48 MiB unpacked (about 58 MiB compressed); larger uploads are rejected with
413 package_too_largebefore the body is read. Localsfs skillimports keep their 128 MiB limit. - Each API process accepts a small number of publishes at a time and one per user; beyond that it answers
503 publish_busywith aRetry-Afterheader. An upload that stalls or takes longer than two minutes gets408 publish_timeout. - Personal packages count against your plan’s storage (Starter 500 MB, Pro 2 GB, Team 1 GB per user, Enterprise unlimited). Organization packages count against the organization’s storage, together with members’ synced sessions.
- Every package keeps at most 100 stored versions, and personal packages share a limit of 50 stored versions per user across all of them. Delete an unreferenced version to free a slot.
Uploads must be plain ustar/GNU tar.gz archives without PAX or GNU extended headers (no x/g/L/K records): member paths must fit the ustar name/prefix fields, and archives created with tar --format=ustar or Python’s tarfile USTAR_FORMAT/GNU_FORMAT with short paths qualify. Extended-header archives are rejected outright because their metadata records are read with whatever size they declare.
Publishing from the CLI
Section titled “Publishing from the CLI”Validate a package before you publish it. sfs skill validate runs the server’s publish checks locally: archive format, path safety, size caps, the plugin.json schema, SKILL.md frontmatter, MCP credential literals, and the secret/PHI scan. The server and the CLI share one validator, so they give the same answers:
sfs skill validate ./review-helper # personal publishingsfs skill validate ./review-helper --visibility org # also requires a declared licenseEach problem shows the file and line (when known), why it fails, and how to fix it. Secret and PHI findings are listed by file and line, never by the matched text. Validation exits with code 1 on errors, so it can gate CI. On success it prints the content hash and the risk summary (scripts, hooks, allowed tools, MCP, executable extensions).
sfs skill publish validates the same way, shows a summary (name, version, license, risk flags, size, target), asks for confirmation, then uploads:
sfs skill publish ./review-helper # your personal librarysfs skill publish ./review-helper --org org_123 --yes # an organization, no promptA directory is packed into a deterministic ustar tar.gz, and .git and OS metadata files are left out. Those exact bytes are validated and uploaded, so the content hash shown before upload is the one the server stores. The upload is named after the package, so a new version lands under the same package.
Some limits need server state, so only the server checks them: your plan, your org role, storage quota, version caps, concurrency (503 publish_busy), upload time (408 publish_timeout), and an organization’s own DLP policy. When the server refuses an upload for one of these reasons, the CLI explains what happened and what to do next.
Secret and PHI findings block a personal publish locally. When you publish to an organization, they appear as warnings instead, because the organization’s DLP allowlist may permit the flagged values. The server applies that policy, and it rejects the upload if the policy does not allow them.
Personal registry URLs are recorded as pinned references only; the server never fetches them. Organization registries can be polled by the server (next section).
Polling Git Registries
Section titled “Polling Git Registries”An organization registry on https:// can publish itself: SessionFS polls it and every new Git tag that matches the registry’s pattern becomes a new, immutable version in the organization’s library. Nobody has to republish, and no CI pipeline needs a publishing key.
- Schedule.
poll_interval_secondsdefaults to 900 (15 minutes) forhttps://registries; the minimum is 300, and0keeps the registry metadata-only (nothing is fetched).ssh://registries are never polled. Registries created before polling existed stay metadata-only until an owner or admin turns polling on. An organization can poll at most 10 registries by default (409 poll_registry_limit), and each scheduler run polls at most two registries per organization so no organization starves the others. - Which refs.
tag_patterndefaults to semver (v?\d+\.\d+\.\d+, matched against the whole tag name). Patterns use a restricted syntax: literals,.,\d,\w, character classes and the quantifiers? * + {n,m}; groups, alternation and backreferences are refused so a pattern can never be made slow. Withbranch_only_modeand abranch, the branch head is published instead; commits between two polls are not published individually. - What gets published. A repository with a
marketplace.json(.claude-plugin/,.codex-plugin/or the root) publishes each plugin that lives in the repository; plugins that point at another repository are skipped (the server never follows URLs found inside a repository). A repository without a manifest is published as one package. Every package goes through exactly the checks of a manual organization publish: validation, the organization’s DLP policy, instruction scanning, disclosure, quotas and moderation blocks. The organization owner or admin who last saved the polling settings is the publisher of record and must still hold that role; if they leave, any owner or admin re-saving the settings takes over. - Immutability. Re-polling the same commit is a no-op. A tag that is force-moved to a different commit produces a new version with its own content hash; existing versions are never changed. A refused package (for example a DLP finding) is recorded against its tag and not retried until the tag moves.
- Private repositories. Set
credential_secret_refto a reference, never a token:env:SFS_REGISTRY_CRED_<NAME>ork8s:<secret-name>. The operator creates the credential and binds it to your organization and to the hosts it may be sent to; any problem with a reference shows one generic message (the operator’s logs say why). Credentials never appear in the URL, in API responses, in poll errors or in logs. - Status. The registry listing and the dashboard show
last_poll_at,last_poll_status(ok,partialwhen some packages were refused,error), a sanitizedlast_poll_error_summary,consecutive_failuresandnext_poll_at. Failures back off exponentially (twice the interval, then four times, up to 24 hours). Network, TLS and server errors are retried; only a refusal of the content itself is recorded against a tag. Poll now in the dashboard (orPOST /api/v1/orgs/{org_id}/skills/registries/{registry_id}/poll, at most once a minute) moves the registry to the front of the next run;GET .../poll-refslists what each tag produced.
curl -X PATCH "$API/api/v1/orgs/$ORG/skills/registries/$REGISTRY" \ -H "Authorization: Bearer $SFS_KEY" -H "Content-Type: application/json" \ -d '{"poll_interval_seconds": 900, "tag_pattern": "v\\d+\\.\\d+\\.\\d+", "credential_secret_ref": "k8s:acme-gitlab"}'The fetch is hardened: https only, the host’s addresses are checked (private, loopback, link-local and metadata addresses are refused; self-hosted operators can allow specific private ranges) and Git is pinned to them, redirects are not followed, hooks never run, nothing is checked out, and size, file-count and time limits apply. Polling never runs inside your API requests; the scheduler calls a dedicated endpoint. See Self-Hosted for operator setup.
Instruction Scanning
Section titled “Instruction Scanning”Every published version is scanned for dangerous instructions and scripts: fetch-and-run, encoded execution, credential harvesting, exfiltration endpoints, permission bypass and hook auto-approval, persistence, concealment, defense evasion, and hidden or bidirectional Unicode. Critical findings refuse the publish; other findings are shown on the listing and in the install disclosure.
Live versions are rescanned weekly and whenever the ruleset changes. A new critical finding quarantines the version: no new installs, downloads, approvals or MCP loads until a moderator blocks or restores it. Existing installs are warned, never removed by a quarantine alone.
Reports, Takedown and Revocations
Section titled “Reports, Takedown and Revocations”Anyone signed in can report a listing. SessionFS moderators, and an organization’s owners and admins for their own listings, review reports in the dashboard moderation queue and can act on a version or a whole package with a required reason and an audit record:
- Yank keeps installed copies but refuses new installs.
sfs skill listandsfs skill doctorshow a warning. - Block removes the package from every device.
sfs skill pulland the daemon follow the revocations feed and move blocked packages into~/.sessionfs/skills/quarantine/, keeping any local edits there.
Blocked content is refused by content hash, so deleting and re-publishing the same bytes does not bring it back.
Verified Publishers
Section titled “Verified Publishers”A publisher owns a globally unique marketplace namespace, personal or organization-owned. Verification proves who the publisher is and shows as a badge on its listings, such as verified: acme.com or verified: github.com/acme.
sfs skill publisher claim acme --org org_123 # claim a namespacesfs skill publisher verify pub_abc --dns acme.com # prints a TXT record to addsfs skill publisher verify pub_abc --github-org INSTALLATION_IDsfs skill publisher check pub_abc VERIFICATION_ID # re-check nowDNS verification uses a sessionfs-verification=... TXT record; a domain your organization already verified for SSO is reused. GitHub verification goes through one of your own linked SessionFS GitHub App installations and needs a recent proof that you own or administer it. If a DNS record goes missing for seven days, the badge is dropped until it is back.
Names that imitate a reserved brand or a verified publisher (lookalike letters from any script, digit swaps, typos, -official suffixes) are refused, and display names never carry a badge.
Trusted Publishing from GitHub Actions
Section titled “Trusted Publishing from GitHub Actions”Trusted publishing lets a GitHub Actions workflow publish without a SessionFS API key stored in the repository.
-
Allow a specific repository and workflow, ideally with a protected GitHub environment and a ref pattern:
Terminal window sfs skill publisher trust pub_abc --repo acme/review-helper \--workflow publish.yml --environment release --ref-pattern 'refs/tags/v*' -
In the workflow, give the job
permissions: id-token: writeand run:Terminal window sfs skill publish . --trusted-publishing --namespace acme
The CLI exchanges the job’s OIDC token for a single-use publish token that expires after 15 minutes. Pull-request events never publish. Each listing shows where the version was published from: repository, workflow, ref, environment and run. Remove a trust with sfs skill publisher untrust; tokens already issued under it stop working.
Trusted Publishing from GitLab CI
Section titled “Trusted Publishing from GitLab CI”GitLab CI jobs can publish the same way, using a GitLab CI ID token (the id_tokens: keyword; the removed CI_JOB_JWT variables are not supported).
-
Allow a GitLab project by its numeric id (Settings > General), with a ref pattern and, ideally, an environment:
Terminal window sfs skill publisher trust pub_abc --gitlab-project 12345678 \--gitlab-project-path acme/review-helper \--ref-pattern 'refs/tags/v*' \--environment production --require-protected-environmentThe command also pins the project’s current namespace (group) id, read from the GitLab API (
GET /api/v4/projects/<id>; public projects need no token, andGITLAB_TOKENorCI_JOB_TOKENis used when set). A project keeps its numeric id when it is transferred to another group, so without the pin that group’s protected-ref pipelines would still match. If the lookup fails the command refuses: pass--gitlab-namespace IDto set the id yourself, or--no-namespace-pinto skip pinning (a warning is printed, the server flags the trust, and the dashboard shows “Namespace not pinned”).For self-managed GitLab add
--gitlab-host https://gitlab.example.com; the server operator must list that instance inSFS_GITLAB_OIDC_ISSUERS(onlyhttps://gitlab.comis allowed by default), exactly as it appears in the token’siss(GitLab’sexternal_url: lowercase host, no:443, no trailing slash).A job names its own environment, so
--environmentonly adds security when that environment is protected in GitLab (a GitLab Premium feature).--require-protected-environmentrefuses tokens unless GitLab reports the environment as protected; leave it off on GitLab Free and rely on protected tags. -
Protect the tags or branches you publish from. SessionFS only accepts jobs running on a protected ref.
-
In
.gitlab-ci.yml, request an ID token for this server’s audience (theaudiencefromGET /api/v1/marketplace/trusted-publishing/config) and publish:publish-skill:environment: productionid_tokens:SFS_ID_TOKEN:aud: https://api.sessionfs.devrules:- if: $CI_COMMIT_TAG =~ /^v/ && $CI_PIPELINE_SOURCE == "push"script:# sessionfs==<version you tested>, hash-pinned; token kept out of pip.- env -u SFS_ID_TOKEN python -m pip install --require-hashes --only-binary=:all: -r requirements-publish.txt- sfs skill publish . --trusted-publishing --namespace acmeThe ID token is live for the whole job, so declare
id_tokens:only on the publish job and install an exact, hash-pinned SessionFS release.
The server checks the token’s signature against the instance’s published keys, the audience, the numeric project id, the protected ref and ref pattern, the environment, and the pipeline source: only push, web and api pipelines publish. Merge request, external pull request, child and downstream, trigger-token and scheduled pipelines never publish. GitLab mints the token when the job starts and SessionFS accepts it for 60 minutes, so keep publishing in its own short job. The token exchange returns the same single-use, 15-minute publish token, and each ID token can be exchanged once.
Launch Catalog
Section titled “Launch Catalog”The public catalog starts with sessionfs-workflows, an MIT-licensed package written by SessionFS. It contains four instruction-only skills with no scripts, hooks, or MCP servers:
| Skill | What it does |
|---|---|
session-handoff |
Hand a session to a teammate or another tool, or pick one up |
ticket-workflow |
Work a SessionFS ticket from start to review |
project-knowledge |
Load project context and record what you learn in the knowledge base |
worker-review-loop |
Run brief-driven workers in isolated worktrees, gated by a trusted reviewer and a re-review before merge, recorded with one call per worker run and review round (see Worker Review Loop) |
Find it under Marketplace in the dashboard. It reaches your tools the same way as any other package: request it for a project, load it through the target feed, or assign it to your organization. The source is also in the SessionFS repository under catalog/sessionfs-workflows, so you can also copy it into a project and run sfs skill add ./sessionfs-workflows.
Org Skills Policy (Enterprise)
Section titled “Org Skills Policy (Enterprise)”An Enterprise organization writes one skills policy and SessionFS applies it everywhere it can:
- On the server. Install requests, approvals, assignments, assignment updates and MCP instruction loads are refused when the org’s current policy forbids the version. The refusal names the org and says to ask an org owner or admin. A policy change also applies to grants made earlier: the next reconcile withholds them.
- In each tool’s own managed settings. The policy compiles into Claude Code and GitHub Copilot
managed-settings.json, a VS Code device policy, the Gemini CLI systemsettings.jsonand the Codexrequirements.toml. Cursor gets a checklist for its admin dashboard. For every field SessionFS shows whether the tool enforces it, whether only SessionFS checks it, or whether nothing checks it, so advisory controls are never presented as enforcement.
The policy sets a mode (open, allowlist, library_only or disabled), allowed publishers and git sources (a git source matches only versions published from that repository through trusted publishing, never a repository an uploader merely declares), capability limits (hooks, scripts, local and remote MCP servers and their origins, hidden Unicode), a signature requirement and the highest scanner severity accepted. Every change is a new, immutable revision. Settings with side effects in a tool, such as stopping users’ own hooks, must be acknowledged before a revision is published.
Delivery starts in advisory mode: sfs skill pull and the daemon skip what the policy refuses and report each device’s compliance, including whether IT deployed the compiled managed settings. A user can bypass advisory delivery on their own machine, so deploy the compiled files through your MDM for tool-level enforcement:
sfs policy show # what applies to you, per toolsfs policy compile --tool claude-code > managed-settings.jsonsfs policy status # system or advisory, per toolsfs policy set --org org_123 --file policy.json # owners and adminsIf the organization later leaves Enterprise, its last policy stays enforced: owners and admins can still read it and can relax or disable it (sfs policy disable --org ORG), but not tighten it.
Signed MDM bundles are planned. See the CLI reference for the full policy format.
Service Keys
Section titled “Service Keys”Automated clients should use the minimum required scopes:
marketplace:readfor search, inspect, preview, download, and feedsmarketplace:writefor package publishingskills:readfor assignments, requests, and approved instruction loadingskills:requestto create/cancel requestsskills:approveto approve project requestsskills:adminfor organization registries, packages, assignments and the skills policydevices:reportto send skills-policy compliance reports (for example from CI runners)
Service keys remain organization-bound. A key cannot cross its organization even if it knows another resource ID.