skillrepo
v4.22.1
Published
Pull-based CLI for agent skills — init, sync, search, add, remove your library, synced across your dev environment
Readme
skillrepo
A pull-based CLI for managing your agent-skills library from the terminal.
npx skillrepo initValidates your access key, picks which agents to configure, wires up the MCP config, and pulls your library. Safe to re-run at any time.
Install
# One-off via npx (recommended)
npx skillrepo <command>
# Or install globally
npm install -g skillrepo
skillrepo <command>Requires Node.js 18 or later.
Quick start
- Run
initinside your project directory:
When no key is configured,npx skillrepo initinitasks for your access key. Paste it if you have one; otherwise press Enter and it opens skillrepo.dev/cli/auth in your browser, where you sign in and get a key to paste back. The page works headless too — copy the key from any browser if your terminal can't auto-launch one. - Your library is now on disk. The CLI wrote skills into
.claude/skills/(for Claude Code) and.agents/skills/(the shared path for Cursor, Windsurf, Gemini CLI, Codex CLI, Cline, VS Code + Copilot, and others). Both paths are added to.gitignore— skills are a per-developer cache, not committed. - From now on, use
skillrepo updateto pull new versions,skillrepo addto add skills to your library, andskillrepo listto see what you have.
If you'd rather provision the key yourself, create one at skillrepo.dev/app/settings (Settings → Access Keys) and pass
--key <sk_live_...>toinit.
Commands
init — first-run setup
skillrepo init [--key <key>] [--url <url>] [--yes] [--force] [--agent <list>] [--global] [--json] [--no-session-sync]Validates your access key, picks which targets to configure (interactive
two-row picker by default — Claude Code at .claude/skills/ and the
shared .agents/skills/ cohort path for everything else), writes the
MCP config, installs the Claude Code SessionStart hook (opt-in — see
session-sync below), and runs the first library sync.
Detection drives the picker's pre-checked rows. Three signal types per agent are probed:
- Active session env vars (e.g.,
CLAUDECODE,CURSOR_AGENT,GEMINI_CLI,CLINE_ACTIVE) — strongest signal, indicates the agent is currently running. - HOME traces (e.g.,
~/.claude/,~/.cursor/,~/.codeium/windsurf/) — installed-on-this-machine signal. - Project dotfiles (e.g.,
.claude/,.cursor/,.github/skills/) — configured-for-this-repo signal.
When any signal fires for either target, that row pre-checks. Fresh clone with no detection: both rows still pre-checked — the product's job is to set up skills, defaulting to "do nothing" because env vars didn't fire is wrong. Pick the third row ("None") to skip placement and just write the config + gitignore.
| Flag | Description |
|------|-------------|
| --key, -k <key> | Access key. Falls back to SKILLREPO_ACCESS_KEY env var, then interactive prompt. |
| --url, -u <url> | Server URL. Defaults to https://skillrepo.dev. Use for self-hosted. |
| --yes, -y | Non-interactive. Skip all confirmation prompts. Required for CI. Installs the session-sync hook by default — pass --no-session-sync to opt out. Under npx, this also auto-runs npm install -g skillrepo@<this-version> if no global install is present (so the session-sync hook can use the resulting absolute path). |
| --force | Re-prompt for a new key even if ~/.claude/skillrepo/config.json is valid. |
| --agent <list> | Comma-separated agent target override. Canonical tokens: claude (Claude Code), agents (every other supported vendor — Cursor, Windsurf, Gemini CLI, Codex CLI, Cline, VS Code + Copilot — sharing the .agents/skills/ cohort path), none (skip placement entirely; init still writes config + gitignore). Vendor names like cursor or claude-code are accepted as silent aliases. |
| --global | Write skills to ~/.claude/skills/ (personal) instead of .claude/skills/ (project). |
| --no-session-sync | Skip step 6 (SessionStart hook install). Works in both interactive and --yes modes. Use for CI scripts that bootstrap a project without ever starting a Claude Code session. |
| --json | Emit a structured JSON summary on success. Requires --yes. See JSON output shape below. |
JSON output shape (init --json --yes)
{
"action": "initialized",
"account": { "slug": "...", "id": "...", "tier": "publisher|developer|team" },
"config": { "action": "created|updated|unchanged" },
"vendors": ["claudeCode", "cursor", "..."],
"mcp": {
"merged": [".mcp.json", ".cursor/mcp.json"],
"skipped": ["..."],
"failed": [{ "path": "...", "reason": "..." }]
},
"sessionSync": {
"action": "installed|updated|unchanged|opted-out|declined|not-applicable|skipped|failed",
"path": "...",
"cohortHooks": [
{
"vendorKey": "cursor|gemini|codex|copilot",
"displayName": "...",
"path": "~/.cursor/hooks.json",
"action": "installed|updated|unchanged|failed",
"reason": "..."
}
]
},
"sync": {
"added": 0,
"updated": 0,
"removed": 0,
"notModified": false,
"fullSync": true,
"syncedAt": "2026-05-01T00:00:00.000Z",
"globalBoundary": {
"total": 0,
"counts": { "global_library": 0, "global_foreign": 0, "global_shadowed": 0 },
"names": [],
"skillsetDeclared": false
},
"changes": [
{ "kind": "added", "owner": "acme", "name": "alpha", "scope": "project", "fromVersion": null, "toVersion": "1.0.0", "contentChanged": true }
],
"verifiedAt": "2026-05-01T00:00:00.000Z",
"priorRecord": { "verifiedAt": null, "attemptedAt": null }
}
}sync.globalBoundary (4.13.0) appears on every successful
project-scope sync and describes the personal-scope skills that also
load in sessions here; it is absent when the scan did not run
(--global, or a scan failure). names stays local to your machine —
sync reporting to your organization carries counts only.
sync.changes, sync.verifiedAt and sync.priorRecord (4.22.0) are the
sync engine's own report of the run — the same facts the session-start
status line shows. changes lists one entry per skill the run changed
(kind: added, updated, restored, removed, quarantined; a
byte-identical rewrite on a full re-fetch is not an entry, unlike the
updated counter). Each entry's contentChanged says whether the content
placed differs from what the CLI's record held for the skill — not whether
files moved: a restored folder re-placed with the recorded content reads
false, a removal reads true. verifiedAt is the timestamp the run
stamped on its record on a successful 304 or complete 200. It is null
when the run reached the registry but did not persist — then
incomplete: true and incompleteReason (partial-payload, invalid-skill,
missing-etag, state-write-failed, no-prior-state) say why — and also
null, without incomplete, when the registry was not contacted at all
(throttled, servedLastVerified). kind and incompleteReason are open
enums — tolerate values you do not know. priorRecord carries the record's
verifiedAt / attemptedAt as they stood before the run. skillrepo update
--json prints the same summary. changes, verifiedAt and priorRecord
are absent from the two summaries init synthesizes without running the
engine: --agent none (the sync is skipped) and a first sync that failed
(sync.failureReason).
Field notes:
vendorsis the resolved canonical-key list, NOT the raw--agentinput.--agent agentsproduces every cohort vendor (cursor, windsurf, gemini, codex, cline, copilot);--agent noneproduces an empty array.sessionSync.cohortHooks[]reports per-vendor outcomes for the auto-refresh hooks installed alongside the Claude Code SessionStart hook (one entry per cohort vendor with a non-nullagentHookregistry spec — Cursor, Gemini CLI, Codex CLI, VS Code + Copilot).reasonis present only whenaction: "failed". Empty array when--no-session-syncwas passed or no cohort vendor was selected.sync.fullSyncistruefor first-time syncs (no prior.last-sync),falsefor delta syncs, andnullonly on synthesized-failure summaries when the network call never completed (also signals viasync.failureReason).sync.failureReason: stringis present only when the first sync failed but the rest of init succeeded — config is still saved; user should re-runskillrepo update.
init is idempotent: re-running with a valid existing config re-runs
detection + MCP merge + first sync without re-prompting for a key. If the
stored key has been revoked (401 from /auth/validate), the CLI falls back
to the interactive prompt automatically; pressing Enter there opens
skillrepo.dev/cli/auth to get a fresh
key.
When stdin is not a TTY (CI, piped input), init skips the browser launch
and just prints the URL — paste the key via the upstream pipe.
Headless / non-interactive: under --yes the picker is skipped and
the pre-checked rows become the selection. With no detection signals,
both default rows are pre-checked — running init --yes on a fresh
clone configures both targets so CI bootstrap scripts always produce
a working library. Pass --agent <target> (claude, agents, none,
or a vendor name like cursor) to bypass the picker entirely.
update — sync your library
skillrepo update [--global] [--agent <list>] [--json] [--silent]Pulls the latest state of your library from the server using a delta
sync. Writes new and updated skills, removes skills that left your
library, and skips skills that are unchanged. Removals apply on both the
delta sync and the periodic full re-sync (the one that runs on a first
sync, after a CLI upgrade, or when a new editor is detected), so a skill
your team recalled or deleted stops loading everywhere rather than
lingering on disk. If you had edited a removed skill locally, its folder
isn't discarded — it is moved to a quarantine area
(~/.claude/skillrepo/quarantine/) with a warning, so it leaves the
folders your agents load while your changes are preserved. In a repo
that declares a skillset, update instead reconciles that declared set
(a full-set resolve, not a delta). Uses ETag caching so repeat runs
return 304 Not Modified when nothing has changed.
--silent suppresses normal output and writes a single {} line to
stdout. It is what the Cursor hook runs (Cursor has no user-visible
hook output), and what Gemini, Codex and Copilot hooks written before
v4.21.0 still run. Sync progress and warnings go to stderr. Since
v4.21.0 a failure also goes to stderr as one line and the process still
exits 0 and still prints {} — a hook exit never ends a session on any
host (it used to propagate the error's exit code; an exit of 2 is what
VS Code treats as a blocking error).
--agent-hook <gemini|codex|copilot> (v4.21.0+) is what the other
cohort hooks run. Same sync as --session-hook, always exit 0, one
JSON document whose systemMessage is the status line: Gemini CLI
shows it at session start, Codex CLI shows it as a warning, and VS Code
shows it when it runs the Copilot hook file (the Copilot CLI consumes
only the agent's additionalContext, which the Copilot document carries
in both hosts' shapes). A failed sync shows its ⚠ line instead of ending
the hook. An unknown vendor prints a readable failure line and exits 0.
The command needs skillrepo 4.21.0 or newer to be the version npx
resolves; an older binary (a project-local node_modules/.bin/skillrepo,
or a registry mirror that lags) rejects the flag as an unknown argument
and the host shows that as a hook warning until the install catches up.
--session-hook (what the Claude Code SessionStart hook runs) prints one
JSON document on every run, success or failure, and always exits 0:
{
"systemMessage": "✓ SkillRepo · in sync · 37 skills (31 project · 6 user-level) · verified 4 min ago",
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"reloadSkills": true,
"additionalContext": "[SkillRepo] 6 global skills will also load in this session: …"
}
}systemMessage is the status line Claude Code shows you (see
session-sync below for what it says). additionalContext is what the
agent reads — the global-boundary disclosure and, in a declared repo,
the compliance state — unchanged from earlier versions, which printed
them as plain text. reloadSkills is present only when skills were
written, so the session's skill discovery picks them up before the
first prompt.
Every sync also inventories the skill folders it manages. A directory
the CLI did not write — a hand-authored or copied-in skill — gets a
one-line warning naming the path, why it matters (agents load skills
from disk whether or not they came from your library), and what to do
about it (skillrepo push <path> to bring it into your library, or
remove the directory). Each finding is reported once: the warning
repeats only when something changes, and only interactive runs print
it (session auto-sync hooks stay quiet). Warnings are local only
(skill names never leave your machine), and nothing is ever deleted or
modified. --json output includes the total count as unmanaged.
Sync also warns — once per repo, same rules — when a skillrepo.json
at the repo root is gitignored: the skillset declaration only works as
a committed file, so remove it from .gitignore and commit it.
Project syncs also disclose the personal scope (the global boundary):
skills under the global folders (~/.claude/skills/,
~/.agents/skills/, ~/.codeium/windsurf/skills/) load in every
session run inside a project, whether or not they came through a
skillset. When any exist, a one-line disclosure says how many global
skills will also load — in a declared repo it also counts how many sit
outside the repo's skillset and how many collide with a skillset
member's name. Today the session auto-sync prints that line in Claude
Code only (the other agents' background sync hooks run silently by
their hook contracts — see docs/vendor-paths.md for each vendor's
channel); skillrepo list shows the same line everywhere. Interactive
syncs add a one-time warning per finding with the remediation, and
your organization's sync reporting carries counts only
(global_library, global_foreign, global_shadowed), never names.
A repository can declare a skillset in a root skillrepo.json:
{"skillset": {"version": 1, "name": "<repo-identity>", "use":
"owner/skillset-name"}} (optional extra: ["owner/skill"]). The CLI
finds the file from any subdirectory (it walks up to the repository
root), validates it, and syncs that repository against the declared
skillset instead of your whole library — a declared repo only ever
receives its declared skillset. Skillset-scoped sync resolves the
complete set on every pass: skills added to the skillset appear,
skills removed from it are deleted from the repo's skill folders, and
everything lands at the repository root no matter which subdirectory
you run from. Deletes are guarded — a directory the CLI did not write is
never removed or overwritten; it stays put with a warning and shows up
in your team's sync reporting instead. A skill the CLI wrote that you
then edited is moved to the quarantine area
(~/.claude/skillrepo/quarantine/) rather than deleted when it leaves
the set, so it stops loading while your edits are preserved.
Declared entries in extra that cannot be honored (for example when
extras are not enabled for your organization) are skipped loudly with
the reason, never silently. If the registry is unreachable, the repo
keeps serving its last-verified skills and the sync still exits
successfully with a warning (the warning escalates once the state is
more than ~72 hours old). If the declaration itself cannot be honored
— unknown skillset, skillsets not enabled for your organization, an
invalid file — the sync fails closed with exit code 6 and nothing is
written or removed. A skillrepo.json without a skillset key is
plain configuration and does not change sync. See
docs/skillset-declaration.md for the
full file-format reference.
get — fetch a single skill
skillrepo get <@owner/name> [--global] [--agent <list>] [--json]One-shot fetch. Does NOT mutate your library or the server — just reads
GET /api/v1/skills/{owner}/{name} and writes the skill to disk. Use
this to preview or pin a specific skill without adding it to your library.
list — show what's on disk and how it compares to the server
skillrepo list [--json]list is disk-first: it walks the skills actually on disk, then
annotates each against your last sync (local integrity) and the server
(governance + freshness). This means an on-disk skill is never hidden —
a hand-authored directory, a skill the server has withdrawn, or one you
deleted all show up, alongside the ones that are healthy.
The State column shows one worst-wins signal per row, staying quiet
when a row is healthy:
OK— on disk, matches your last sync, and the server hasn't moved on.EDITED— local files were modified since the last sync;updatewould overwrite them.BEHIND— the server has a newer version than what you synced; runskillrepo update.SERVING— the server rolled a recalled version back to an earlier one; you're being served the fallback.WITHDRAWN— on disk and once served to you, but the server no longer serves it. The CLI can't tell a recall from an unpublish or a skillset re-scope, so it doesn't guess — it flags that it's no longer served (remove it, or investigate).MISSING— expected (in your library, or your repo's skillset) but not on disk.FOREIGN— on disk in a managed directory, but not from your library.NOT DELIVERED— a skillset member the CLI declined to write (edited locally, replaced, incomplete, or invalid), with the reason.NOT CHECKED— the server couldn't be reached, so freshness is unknown.listnever reports a stale copy as current.
list speaks the dashboard's LATEST / SERVING vocabulary (green is
used only for a served head version), so healthy and recall-fallback
states read identically on the CLI and the web. Where the CLI can't verify
a governance cause — a skill that's simply no longer served — it says
WITHDRAWN rather than asserting RECALLED; recalled appears only in a
SERVING row's reason, where the server names the recalled version. Each attention row also carries a
plain-English reason — what changed, why, and what to do — listed in an
attention block beneath the table. Both list views render the same way
(see "Same view everywhere" below).
When multiple vendors are detected, the local integrity axis is a
worst-wins rollup (missing > edited > present).
Same view everywhere. A repo that declares a skillset shows its
delivered set in the identical table — same columns, same colours, same
attention block as the whole-library view — plus a compliance line for
the declared set. list issues a non-recording freshness probe for the
declared set (a full read that resolves and records nothing) so a
recalled-then-restored skill reads BEHIND even inside the sync throttle
window, and a recall fallback reads SERVING rather than a false green.
Offline-capable. list works with no network: the local axes are
computed from disk, and the server axis degrades to NOT CHECKED rather
than failing.
Read-only. list never records a delivery, writes sync state, or
consumes a warning update owes you.
The State chip is coloured to match the dashboard — green for the one
healthy state (OK / LATEST), amber for SERVING, red for WITHDRAWN
and MISSING. Glyphs and colour are used in TTY contexts; plain ASCII
tokens (OK / EDITED / BEHIND / …) with no colour appear when stdout
is not a TTY or NO_COLOR is set, so piped output stays clean.
--json is a bare array of skill objects (a top-level object with a
skillset block in a declared repo). Each row carries the full model —
classification, local, governance, freshness, the collapsed
signal, and a per-vendor placements[] array. The pre-#2806 state
(worst-wins drift rollup) field is kept for backward compatibility, so
existing scripts keep working.
Each row's version is the installed (on-disk) version — what your
last sync wrote — while servedVersion is the version the server
currently serves. They differ whenever you hold a version the server has
moved off (a recall fallback reads SERVING, a stale copy reads
BEHIND), and the reason line for those rows names the served version.
Before 4.17.1 the top-level version reported the served version and
there was no installed-version field, so a script that read version as
"what the server serves" should read servedVersion instead.
search — explore the registry
skillrepo search <query> [--limit <n>] [--json] [--semantic]Queries /api/v1/skills/search for public skills matching query.
--limit caps the result count (default 20). --semantic is accepted
but currently a no-op — semantic search is a planned backend feature.
add — add a skill to your library
skillrepo add <@owner/name> [--global] [--agent <list>] [--json]POSTs to /api/v1/library, then fetches the single skill directly and
writes it to disk. Requires a write-scoped access key. On 409 (already
in library), re-fetches the current version so your local state is
consistent.
remove — remove a skill from your library
skillrepo remove <@owner/name> [--global] [--agent <list>] [--json]DELETEs from /api/v1/library and deletes the local directory. Requires
a write-scoped access key. The local delete is immediate and does not
wait for a follow-up sync.
push — upload a local skill directory to your account
skillrepo push <path> [--idempotency-key <key>] [--key <key>] [--url <url>] [--json]Multipart POST to /api/v1/library. Walks <path>, packages
SKILL.md plus every supporting file under scripts/, references/,
and assets/, and uploads them as a single request. The server picks
the outcome by SKILL.md frontmatter name:
- First push of this name → creates a private skill and returns
action: "created". - Subsequent push with changed content → releases a new version.
The server classifies the bump as major (if
description,allowed-tools, orcompatibilitychanged) or minor otherwise, and returnsaction: "updated"with the bump kind. - Identical content → no-op (SHA-matched). Returns
action: "unchanged".
push is the only CLI command that uploads local content; it does not
write anything back to disk. Use update to pull canonical skills
from the server to your local library.
Limits: total multipart body ≤ 4.5 MB, per-file path depth ≤ 5
segments, executable/archive extensions blocked (full list in
src/lib/skills/constants.ts). Anti-abuse rate limit: 6/min and
100/hr on Developer; 30/min and 500/hr on Team. (push needs a
write-scoped access key, which starts on the Developer plan.)
Flags:
--idempotency-key <key>— explicit key for safe retries. By default the CLI generates a fresh UUID per invocation and uses it for the in-process retry loop (transient 5xx/429 get up to 3 attempts with exponential backoff and jitter). Pass an explicit key to share it across separate shell invocations (CI step retries, manual reruns) and replay the cached response. Cached responses live for 24 hours.--json— emit a structured object (action,bump,owner,name,version,filesUploaded).--key,--url— standard auth / endpoint flags.
Exit codes: 5 (validation — bad SKILL.md, blocked path, payload too
large, plan_limit), 4 (scope — read-only key), 2 (auth), 1
(network/5xx after retries).
publish — make one of your skills visible in the public catalog
skillrepo publish <@owner/name> [--key <key>] [--url <url>] [--json]POSTs to /api/v1/library/{owner}/{name}/publish to flip the
skill's visibility from private to global. Idempotent: calling
publish on an already-global skill returns 200 with
action: "unchanged" (the CLI prints "Already published").
Requires a write-scoped access key.
Permission model: admin+ members can always publish. Non-admin members
can publish if their account-membership has canPublish: true (or the
account-wide memberCanPublish default is enabled). Without either,
the CLI exits 4 / scope with code: publish_not_permitted. The same
canPublish capability gates unpublish and delete symmetrically.
Publish-only preconditions that surface as exit 5 / validation:
namespace_unset— the account's handle is still the auto-generated placeholder; choose an Author ID in Settings first.analysis_pending— safety analysis hasn't completed (only fires where analysis is enabled).safety_grade_too_low— the skill's safety grade isF.
Flags:
--json— emit{ action, owner, name }. The CLI exits non-zero on error, so the presence of stdout JSON already implies success — there is nookfield.--key,--url— standard auth / endpoint flags.
unpublish — remove one of your skills from the public catalog
skillrepo unpublish <@owner/name> [--key <key>] [--url <url>] [--json]POSTs to /api/v1/library/{owner}/{name}/unpublish to flip the
skill's visibility from global to private. Pair to publish:
same auth, same scope, same flag surface.
Subscribers keep their copy. Accounts that already had your skill in their library retain the version they pulled — they just stop receiving future updates unless you republish. The CLI surfaces a one-line summary (e.g. "Notified 12 subscribers") so you know how many accounts the unpublish reached.
Each affected subscriber account's owner-role member(s) receive a
one-time notification email, debounced 24h per (skill, account)
pair so a fast unpublish/republish/unpublish cycle doesn't spam the
same recipient. The publisher's own account is excluded.
Already-private skills (action: "unchanged") trigger no emails.
Flags:
--json— emit{ action, owner, name, notifiedSubscriberCount }.notifiedSubscriberCountis0whenaction === "unchanged". Nookfield — exit code carries success/failure.--key,--url— standard auth / endpoint flags.
session-sync — auto-sync on Claude Code session start
skillrepo session-sync enable [--global] [--json]
skillrepo session-sync disable [--global] [--json]Installs (or removes) a Claude Code SessionStart hook that calls skillrepo update every time you open a Claude Code session — keeping your library current without you remembering to sync manually. The hook lives in .claude/settings.local.json (per-developer, gitignored by default) and runs your existing globally-installed skillrepo binary.
By default skillrepo init prompts you to install this hook. If you said no (or passed --no-session-sync), run session-sync enable later to turn it on.
Auto-refresh hooks for other agents
For Cursor, Gemini CLI, Codex CLI, and Copilot CLI, skillrepo init writes a SessionStart hook to each agent's user-scope hook config so your library stays current across your sessions without a separate command. Each hook runs npx --yes skillrepo update …, so it works without a global skillrepo install: --agent-hook <vendor> for the agents that can show you the status line (v4.21.0+), --silent for Cursor.
| Agent | Hook config path | What you see at session start | Notes |
|-------|------------------|-------------------------------|-------|
| Cursor | ~/.cursor/hooks.json (sessionStart event) | Nothing — Cursor has no user-visible hook output; run skillrepo list | update --silent; timeout: 60 (seconds) |
| Gemini CLI | ~/.gemini/settings.json (SessionStart event) | The status line, every session (Gemini shows a hook's systemMessage at session start) | update --agent-hook gemini; matcher: "*" group filter, timeout: 60000 (milliseconds), entry named skillrepo-update |
| Codex CLI | ~/.codex/hooks.json (SessionStart event) | The status line, every session — Codex styles a hook's systemMessage as a warning. Codex runs no hook until you trust it (/hooks) | update --agent-hook codex; timeout: 60 (seconds). Codex also reads [hooks] from ~/.codex/config.toml; both sources coexist — Codex merges them at runtime, so a hand-written TOML entry won't conflict with our JSON. |
| VS Code + Copilot CLI | ~/.copilot/hooks/skillrepo-update.json (sessionStart event) | VS Code: the status line, every session (its local harness reads this file and shows a hook's systemMessage). Copilot CLI: nothing — it consumes only the agent's additionalContext; run skillrepo list | update --agent-hook copilot; timeout: 60 (seconds). One file, two hosts with different schemas, so the document carries the context in both shapes and always exits 0 (VS Code treats exit 2 as a blocking error handed to the model). |
skillrepo init writes these alongside the Claude Code hook for every selected vendor that publishes a SessionStart-equivalent event, and re-running init upgrades an older --silent entry in place. --no-session-sync skips ALL of them. skillrepo uninstall --global removes them surgically — other tools' entries (1Password, Snyk, your own scripts) in those config files are preserved.
Cloud-agent runners (e.g. GitHub Codespaces, Copilot's cloud agent) read only the committed default-branch content. Because skills sync to a per-developer, gitignored cache, those runners do not see the local skill library — same documented limitation as .agents/skills/ placement. The auto-refresh hooks above are per-developer; they don't run in cloud runners.
Auto-refresh hooks for Windsurf and Cline are not yet supported — those agents lack a documented SessionStart-equivalent event today. Run skillrepo update manually in those environments.
Under npx skillrepo init, the CLI offers to install itself globally. Session sync needs the binary at a stable absolute path (the npx cache path is transient and would break on the next cache eviction). Rather than skipping with a warning the way v3.1.1 did, v3.1.2 prompts during init: "SkillRepo needs a global install to enable session sync. Install skillrepo globally now? (Y/n)" — saying yes runs npm install -g skillrepo@<version> (pinned to the version you just invoked) and then installs the hook with the resulting absolute path. Under --yes the install runs without prompting; under --no-session-sync it's skipped entirely. If the install fails (permissions, network, registry), init prints actionable next-steps and continues; the rest of init still completes successfully.
skillrepo session-sync enable does not auto-install — it's an explicit, deliberate command and assumes you already have a global install. If invoked under npx without a global install present, it returns a clear "install globally first" message rather than mutating your global package set.
The hook cannot block your session. The command it runs is '<path-to-skillrepo>' update --session-hook 2>&1 || true on macOS and Linux, and node "<path-to-skillrepo.mjs>" update --session-hook 2>&1 on Windows — a form Git Bash, PowerShell 7 and Windows PowerShell all run (Claude Code uses Git Bash when it is installed and PowerShell otherwise). The --session-hook flag makes update exit 0 on every failure — network outage, revoked key, disk error, anything — and report the failure in the status line below instead. Since v4.21.0 that guarantee is process-wide: in any hook mode (--session-hook, --agent-hook, --silent), a failure anywhere in the CLI — a broken install whose modules fail to load, a bug outside the sync, an unhandled rejection — prints one ⚠ "status unavailable" line ({} under --silent) and the process still exits 0. The binary is a tiny bootstrap that installs this net before loading anything else, so only a corrupted bootstrap itself is outside it. On POSIX systems the || true shell backstop is appended as belt-and-suspenders; on Windows it's omitted (Windows PowerShell has no ||, cmd.exe no true), so the in-process guarantee is the defense there. Session starts are never blocked by sync failures, on any supported agent: Gemini CLI documents that startup is never blocked, VS Code and the Copilot CLI treat a non-zero hook exit as a non-blocking warning, Cursor's hook is fire-and-forget, and Codex runs no hook until you trust it.
Every session starts with one status line (v4.20.0+). Claude Code shows it as a system message; it is written for you, not the agent:
✓ SkillRepo · in sync · 37 skills (31 project · 6 user-level) · verified 4 min ago
✓ SkillRepo · 2 skills synced · 31 skills from acme/backend · verified just now
+ api-smoke-testing acme project
↻ secure-coding-standard acme-security project 1.3 → 1.4
⚠ SkillRepo · in sync · 31 skills from acme/backend · not compliant: 6 user-level skills beyond the set · verified 4 min ago · skillrepo list for details
⚠ SkillRepo · sync failed: skillrepo.dev unreachable · using 37 skills (31 project · 6 user-level) as last verified 2 h ago · retry: skillrepo update
✓ SkillRepo · 2 skills restored to this project · 37 skills · verified just now
⚠ SkillRepo · sync incomplete: delivery could not be confirmed · using 36 skills as last verified 2 h ago · retry: skillrepo update
⚠ SkillRepo · last sync attempt failed · using 31 skills from acme/backend as last verified 3 h ago · retry: skillrepo update
⚠ SkillRepo · not set up yet · run skillrepo initThe glyph answers "can I trust this session": ✓ means synced and, in a repo that declares a skillset, compliant; ⚠ means something needs attention — a failed, incomplete or stale sync, a skill that could not be written, or a non-compliant repo — and a failure always takes it. "Restored" means the sync re-placed folders that were missing on disk (a fresh clone, a deleted directory) for skills that had not changed upstream. "Sync incomplete" means the registry answered but the delivery could not be confirmed — a skill the server could not fully serve, or one the CLI refused to write (named with its count); the sync retries next session. "Last sync attempt failed" means an earlier session's sync failed and this one was inside the throttle window. "user-level" is the home-directory scope (~/.claude/skills/, what --global writes): in a library repo those skills count toward what loads; in a declared repo any user-level skill outside the set makes the repo non-compliant. "verified" is the time since the last successful registry contact — a throttled session (next paragraph) reports the previous contact's age, never "just now". Changed skills are listed under a synced line, up to 8; more collapse to counts plus skillrepo list. Earlier versions printed nothing on a quiet session; that silence gave no way to tell "nothing changed" from "the hook never ran", so the line now prints every time.
Throttled to skip redundant syncs (v4.9.0+). Because the hook fires on every editor session, the hook-triggered update (--session-hook / --silent) syncs at most once per ~15 minutes: within that window it exits immediately with no network call. This bounds redundant per-session load. The tradeoff is that a brand-new session started shortly after a recent sync may be up to ~15 minutes behind a just-published change — and the same bound applies to local repair: if you delete or corrupt a synced skill's folder on disk, the hook restores it on its next non-throttled run, up to ~15 minutes later. A bare skillrepo update you run yourself is never throttled — it always syncs immediately (repairing any local damage) and resets the window. skillrepo list is likewise never throttled: it reads the server live (a non-recording request) when reachable and degrades to local-only when it isn't, and it never resets the window. A repo that declares a skillset throttles on its own per-repo clock, so an unrelated repo's bulk sync elsewhere on the machine can no longer starve its refresh (v4.16.0+).
Flags:
--global— operates on~/.claude/settings.local.jsonso the hook fires in every Claude Code session across all projects on your machine.--json— emit structured JSON withaction,path, andcommandfields for scripting.
uninstall — remove SkillRepo from a project
skillrepo uninstall [--dry-run] [--yes] [--global] [--json]Surgically removes every SkillRepo artifact from the current project:
mcpServers.skillrepofrom.mcp.json,.cursor/mcp.json, and.vscode/mcp.json(plus the matchinginputsprompt in the VS Code config)SKILLREPO_ACCESS_KEY=...lines from.env.local- The SkillRepo section of
.gitignore - The SkillRepo
SessionStarthook from.claude/settings.local.json(if present) - The
.claude/skills/directory
Non-SkillRepo entries in shared files are preserved. Runs offline — no server call required, so a revoked or missing access key is not a problem. Interactive by default: the command prints a full list of what will be removed and prompts for confirmation before touching anything.
With --global, also removes:
mcpServers.skillrepofrom~/.codeium/windsurf/mcp_config.json- The
~/.claude/skills/global skill cache - The
~/.claude/skillrepo/directory (stored credentials + sync cache) - The SkillRepo SessionStart entry from each cohort vendor's hook config —
~/.cursor/hooks.json,~/.gemini/settings.json,~/.codex/hooks.json, and~/.copilot/hooks/skillrepo-update.json. Other tools' entries (1Password, Snyk, Apiiro, the user's own hooks) and unrelated top-level keys (theme, mcpServers, etc.) are preserved — only our entry, identified by its command (npx --yes skillrepo update --silentor… --agent-hook <vendor>), is filtered out.
Flags:
--dry-run/-n— print what would be removed and exit without touching any file.--yes/-y— skip the confirmation prompt.--global— also remove user-global state. By default the command leaves your credential and other projects' integrations untouched.--json— emit structured JSON instead of human output. The summary includesremoved[]anderrors[]arrays suitable for scripting.
The command is idempotent — a second run with nothing left to remove exits 0 and reports "Nothing to remove." If any artifact fails to remove (e.g. a file is read-only), the command continues processing the others, surfaces every error at the end, and exits with code 3 (disk error).
Configuration
Credentials
The CLI stores credentials at ~/.claude/skillrepo/config.json (chmod
0600 on POSIX):
{
"schemaVersion": 1,
"apiKey": "sk_live_...",
"serverUrl": "https://skillrepo.dev",
"accountSlug": "alice",
"accountId": "acc_...",
"userId": "user_...",
"writtenAt": "2026-04-15T00:00:00Z"
}Every command except init resolves credentials in this order:
--key/--urlCLI flags~/.claude/skillrepo/config.jsonSKILLREPO_ACCESS_KEY/SKILLREPO_URLenvironment variables- Hard-fail with a "run
skillrepo init" hint
init is different by design. Because init owns the credential
lifecycle — it writes the config file in the first place, and
--force / stale-key re-prompts need to decide whether to reuse
cached credentials — init uses a more nuanced order. Resolved
in sequence (each step only runs if the prior ones produced no
key):
--key/--urlCLI flagsSKILLREPO_ACCESS_KEY/SKILLREPO_URLenvironment variables (process env, not files)- Existing
~/.claude/skillrepo/config.json— skipped when--forceis set .env.localand.envfiles in the project directory — skipped when--forceis set- Interactive prompt for the key; server URL falls back to
https://skillrepo.dev
Two scenarios worth calling out:
init --forcewithSKILLREPO_ACCESS_KEYset: uses the env var (step 2).--forceonly clears the cached credentials, not the runtime env.initwith valid existing config and no flags/env: reuses the cached credentials for a silent no-op refresh.initwith a config that the server now rejects (stale key): falls through to the interactive prompt automatically, without needing--force.
Environment variables
| Variable | Purpose |
|----------|---------|
| SKILLREPO_ACCESS_KEY | Access key for any command. Takes precedence over the config file but not CLI flags. |
| SKILLREPO_URL | Server URL. Same precedence as above. |
| SKILLREPO_TIMEOUT_MS | Per-request fetch timeout in milliseconds (default 30000). Set to 0 to disable. |
| SKILLREPO_NO_UPDATE_CHECK | Set to any non-empty truthy value (1, yes, true) to disable the post-command npm-registry self-staleness check. The check otherwise runs at most once per 24 hours, hits registry.npmjs.org/skillrepo/latest, and prints a one-line upgrade hint to stderr if a newer version is available. Auto-disabled when CI=true. |
| NO_COLOR | Set any non-empty value to disable ANSI color in CLI output. |
Update nudge
After every command that isn't --json, the CLI does a best-effort
check against https://registry.npmjs.org/skillrepo/latest and prints
a one-line hint on stderr when a newer version is available:
A newer skillrepo is available: 4.3.0 → 4.5.0
Upgrade: npm install -g skillrepo@latestThe check is cached at ~/.claude/skillrepo/.npm-version-check for
24 hours on success and 1 hour on failure, has a 2-second fetch
timeout, and is fire-and-forget — every failure mode (network error,
non-2xx, parse failure, read-only FS) is swallowed silently. Set
SKILLREPO_NO_UPDATE_CHECK=1 to disable. CI=true auto-disables.
Output is suppressed entirely when --json is on the parent command,
so structured pipelines aren't disturbed.
Skill placement
Skills land at one of two project paths, depending on the agent:
| Agent | Project path | Personal (--global) path |
|---|---|---|
| Claude Code | .claude/skills/<name>/ | ~/.claude/skills/<name>/ |
| Cursor / Windsurf / Gemini CLI / Codex CLI / Cline / GitHub Copilot | .agents/skills/<name>/ | ~/.agents/skills/<name>/ |
| Windsurf (personal-scope override) | — | ~/.codeium/windsurf/skills/<name>/ |
The CLI keeps .claude/skills/ and .agents/skills/ gitignored,
re-checked on every sync: an entry is added to the SkillRepo section of
your project .gitignore only when no existing rule already ignores
that path (a repo that ignores .claude/ gets no .claude/skills/
line) — skills are a per-developer cache, not committed content. The --agent flag overrides detection (e.g.
--agent claude,agents writes both paths). See
docs/vendor-paths.md for primary-source
citations on each agent's read paths.
Exit codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Network error (unreachable server, DNS, timeout, exhausted retries) |
| 2 | Auth error (invalid key, revoked or suspended account, or no access key provided) |
| 3 | Disk error (cannot read or write a file/directory) |
| 4 | Scope error (key lacks the required registry:write scope) |
| 5 | Validation error (bad flag, malformed identifier, unknown vendor, or a prompt whose input ended before it was answered) |
| 6 | Unresolvable skillset declaration — the repo's root skillrepo.json declares a skillset that cannot be honored: an invalid or nested declaration, reserved @ version syntax, an unknown skillset, or skillsets not enabled for your organization. Fail-closed: nothing was written or removed. A registry that is merely unreachable is NOT this class — the sync serves the last-verified skillset and exits 0 with a warning. |
| 130 | Cancelled with Ctrl-C at a prompt |
Pass --verbose to any command to print stack traces and retry
attempts on failure.
Retries
The CLI automatically retries transient server failures on every
idempotent API call — specifically POST /auth/validate,
GET /library, GET /skills/:owner/:name, and GET /skills/search.
This means the read-only commands (update, get, list, search)
and the validate + sync calls inside init benefit automatically.
- Triggered by: 429, 502, 503, 504, and raw network errors (DNS, TCP reset, timeout).
- NOT triggered by: 4xx auth/validation errors — those never succeed on retry — nor 500, which is treated as a real-bug signal that should surface loudly rather than be masked.
- Backoff: exponential with full jitter, 3 attempts total, base 500ms, capped at 8 seconds per sleep.
- Write endpoints (
add,remove) are single-shot to keep behavior predictable — a 503 mid-POST could mean either "body never reached the server" or "body processed, response lost", and the client cannot distinguish the two.
Self-hosted SkillRepo
The CLI works against any SkillRepo instance that implements the v1 API. Point it at your own host:
skillrepo init --url https://skillrepo.internal.company.comOr persist it via env var in your shell profile:
export SKILLREPO_URL=https://skillrepo.internal.company.com
export SKILLREPO_ACCESS_KEY=sk_live_...Troubleshooting
"No access key configured" — Run skillrepo init, or pass --key,
or set SKILLREPO_ACCESS_KEY.
"Invalid access key format" — Keys must start with sk_live_.
The CLI trims leading and trailing whitespace from keys in every
path — both the interactive prompt in init and the Bearer header
used by every subsequent command — so pasting from email or a
browser with a trailing newline is safe.
Picker selected nothing under --yes — Phase 3 (#1236) replaced
the old "no agents detected → refuse" branch with a friendlier
default: when no detection signals fire, both default rows are
pre-checked, so --yes configures both targets. If you want to skip
placement entirely under --yes, pass --agent none (config and
gitignore still happen; only the skill files are skipped).
"Rate limit exceeded — retried automatically and still getting
429" — The CLI already retried with backoff. Wait a minute and
retry. Pass --verbose to see the per-attempt timings.
Windows: skill updates use a best-effort atomic rename pattern.
A crash mid-update may leave a partial state; the next update run
fully overwrites it.
License
Proprietary. Copyright © 2026 SkillRepo LLC. All rights reserved. Use of this CLI is governed by the SkillRepo End User License Agreement at https://skillrepo.dev/eula. See LICENSE for the full notice.
