Introduction
quay is a CLI for sharing skills — small Markdown + YAML packages that teach AI coding agents project conventions, internal APIs, and team workflows — across many repos and many machines without a hosted registry.
Skills live as plain files in your repo at .agents/skills/<name>/SKILL.md. quay synchronises them with a hub: any git repository you control (GitHub, GitLab, Azure DevOps, Bitbucket). Push a skill to the hub; teammates quay add <name> it from a different project on a different machine. Mirror directories (.claude/skills/, .cursor/rules/, etc.) stay in sync automatically.
Why not a registry-as-a-service?
- No third-party server. Your hub is a git repo you already trust. No new accounts, no rate limits, no vendor lock-in.
- Audit-friendly. Every push is a commit. Every install reads a
registry.jsoncommitted in plain text.git logis your history. - Air-gap-friendly. Self-host the hub on Gitea, GitLab CE, or a Bitbucket server. quay does not phone home.
- Filesystem-first. Skills are files. Edit them with
$EDITOR.git diffworks. No special API.
Who this is for
- Engineers who run Claude Code, Cursor, or other agentic tools and want skill libraries that travel with the team — not the laptop.
- Teams running a mixed-provider stack (work GitHub + personal GitLab + customer Azure DevOps) who need profile-scoped configuration.
- Tooling authors building project bootstrapping flows (CI runners, devcontainers, ephemeral envs) that need declarative skill installs.
What's next
- Getting started — install, first profile, first push, first install. ~5 minutes.
- Concepts — the small vocabulary you need before reading the rest of the book.
- Tutorials — narrative walkthroughs.
- CLI reference — per-command detail.
Getting started
Five minutes from zero to a skill shared between two projects.
1. Install
Homebrew:
brew install evgeniiPerov/tap/quay
Or download a release binary from https://github.com/evgeniiPerov/quay/releases and put it on your PATH.
Verify:
quay --version
macOS Gatekeeper: the first run may be blocked. Right-click the binary → Open once, or
xattr -dr com.apple.quarantine /opt/homebrew/bin/quay.
2. Create a profile
A profile holds your default hub URL, default branch, push mode (PR or direct), and provider kind. The interactive wizard sets everything in one go:
quay profile add -i
Pick a name (e.g. work), paste your hub URL (e.g. git@github.com:my-org/skills-hub.git), answer two more prompts. Done.
Confirm it took:
quay profile show
3. Author a skill
From any project directory:
mkdir -p .agents/skills/hello
$EDITOR .agents/skills/hello/SKILL.md
Paste a minimal frontmatter skill:
---
name: hello
description: Say hello.
version: 0.1.0
---
# Hello
Print "hi" when the agent starts.
4. Push
quay push hello
quay clones your hub into a temp dir, drops the skill into skills/hello/SKILL.md, updates the hub's registry.json, commits, and (depending on push_mode) opens a PR or pushes the commit directly. The URL is printed on success.
5. Install in a different project
In a freshly-cloned repo on another machine:
quay add hello
The skill lands at .agents/skills/hello/SKILL.md. Mirror dirs (.claude/, .cursor/, etc.) are populated automatically based on your profile.
Next steps
- Author your first skill — same flow, with the why behind each step.
- Multi-provider setup — run work GitHub + personal GitLab on one machine.
- Concepts — the terms used in the rest of the book.
Concepts
Short definitions for the vocabulary used in the rest of the book.
Skill
A directory under .agents/skills/<name>/ containing at least SKILL.md. The Markdown body is what an AI agent reads; the optional YAML frontmatter (name, description, version, tags) gives quay structured metadata.
quay tolerates three skill formats:
- Frontmatter — YAML block at the top. Versioned, tagged, the recommended form.
- SlashCommand — single-line H1 in the form
# /<name>. Convention for slash-command-style skills. - Freestyle — anything else. Treated as a skill with defaults (dir-name = name, version =
0.0.0).
Hub
Any git repository you control. quay reads and writes skills/<name>/SKILL.md plus a top-level registry.json describing the set. A hub is just a repo — clone it, browse it on GitHub, run CI against it.
Profile
A named bundle of defaults in ~/.config/quay/config.toml: the default hub URL, branch, push_mode, mirror dirs (.claude/skills/, .cursor/rules/, …), and a provider hint. Switch with quay profile use -i.
Remote
A profile's hub URL plus its provider kind (one of github, githubenterprise, gitlab, bitbucket, azuredevops). Per-command override: quay push --remote work-gitlab ….
Provider
The plug-in that knows how to talk to a specific git host: how to parse the URL, how to open a PR (gh, glab, az), how to construct a fallback compare URL, and how to run a connectivity check. Bitbucket has no CLI binding; quay always falls back to a compare URL there.
Mirror dirs
Tools like Claude Code read skills from .claude/skills/, Cursor from .cursor/rules/. quay keeps copies of installed skills synced from the canonical .agents/skills/ into these mirror dirs based on each profile's mirrors setting.
Registry
registry.json at the hub root. A { skills: { <name>: { version, … } } } map describing the hub's contents. quay search and quay add read it. quay rebuild-registry <remote> regenerates it from skills/*/SKILL.md if it ever drifts.
Push log
~/.config/quay/push-log.json. An append-only record of every successful push: skill name, remote, commit SHA or PR URL, timestamp, source project path. Drives the Dashboard "recent pushes" panel and pushed-local / pushed-direct status badges.
Drift detection
quay outdated and quay scan compute a SHA-on-the-fly for every installed skill and compare to the hub's recorded SHA. A modified local file gets the installed-modified badge; the hub having a newer commit gets outdated. quay add --force overwrites local drift.
push_mode
Per-remote setting controlling commit behaviour:
pr(default) — quay creates a feature branch, pushes it, and opens a PR via the provider CLI (gh,glab,az). Bitbucket falls back to a compare URL.direct— quay commits straight to the default branch andgit push. Used for repos with no branch protection where review overhead is not worth it.
Override per-call: quay push --push-mode direct.
Default-interactive
Most CLI commands detect a TTY and prompt for missing arguments. Pipe them into xargs / scripts and they switch to non-interactive: quay push hello < /dev/null errors instead of prompting. The [i] indicator in --help output marks flags that have wizard equivalents.
Releases
Current and past releases. Binaries for every supported platform are on the
GitHub releases page;
see Install for brew, PowerShell and manual options.
To check what you are running:
quay --version
Notable changes per release. This file is also the source of the GitHub release
notes — dist reads the section matching the version being tagged.
0.16.0 — 2026-10-03
Fixed
- Cancelling a prompt now fails the run. Ctrl-C at the extra-files prompt
(
update,add --force) or atadd -i's per-skill collision prompt used to skip ahead and exit 0, so a wrapping script saw success with skills left untouched. It now stops and exits non-zero withinterrupted, instead of a made-upio error at prompt.update -ialso exits non-zero when any selected skill failed. - Copy mirrors follow an update. A copy-strategy mirror kept the old
version — and any file
--delete-extrahad just removed — with a warning. Mirrors now record a hash of everything quay wrote into them, so one nobody touched is refreshed automatically; one you changed in any way, dotfiles included, is still left alone, with the warning on stderr even under--json.quay link checkreports an out-of-date copy. A copy mirror made by an older version is picked up the first time it matches canonical; one that already differs needs a singlequay link --force. - Copy mirrors keep symlinks as symlinks. They were dereferenced into plain files, so a skill containing a symlink never matched its mirror.
- A failed multi-skill
updatestill reports what it changed. The skills updated before the failure — and theirdeleted_extras— are printed before the error, instead of nothing. - An update never touches a directory named
.<skill>.replaced. The old install is parked in a fresh, uniquely named directory instead of a fixed name that was cleared first. - Files with non-UTF-8 names are never offered for deletion. Two such names could map to the same entry, and deleting it removed both.
Added
quay update --jsonreportsdeleted_extrasper skill — what was actually removed, as reported by the install itself.quay diff --jsonreportscommits_aheadandlast_commit_dateforhub_newer.
Changed
quay-core:FolderReport::head_hash→hub_hash,ReconcileReport::head_bytes→hub_bytes, matching the CLI's naming.SkillManager::add_with_extras/update_one_with_extrasreturn the paths they deleted.
Docs
- Eight recipe pages written; corrected
add,remote,profile,updateand the GitHub tutorial where they disagreed with the CLI.
0.15.3 — 2026-08-03
Performance
-
quay diffno longer spends a network round-trip per file. Comparing a skill directory read one git process per file, plus another to establish that the file existed — and because quay clones the hub without file contents and fetches them on demand, each of those reads was its own request over the wire. A twenty-file skill checked against fifty candidate commits meant thousands of processes and as many requests, and against a large hub the command could take minutes.A revision is now read in one pass: a single listing names every file and every object in it, and a single batch read returns their contents. Batching the reads was not enough on its own — git's batch reader still fetches missing objects one at a time — so quay asks for the whole revision's objects in one request first. A clone that already holds them skips that step without opening a connection, so nothing is slower than it was.
Fixed
- A failed read now names the skill it could not read. When the hub becomes
unreachable mid-comparison, the failure used to surface as raw git output with
no indication of which skill or which revision produced it — and
quay diffcan hit it fifty times per skill. It now says both, and points at hub access.
Internal
- Listing and reading a harbor directory are one operation rather than two. The rule for turning a repository path into a skill-relative one had been written out in three places; no caller applies it any more.
- The batch-response decoder is a pure function with its own tests, and it checks each record against the object that was asked for. Pairing content with paths is positional, so a single misaligned record would file one file's bytes under another file's name and render a confident diff between two files that are identical.
- The folder-level test suite now pins the server capability that decides whether a clone is really content-free, so those tests exercise the path production takes instead of depending on the developer's git configuration.
0.15.2 — 2026-07-31
Fixed
quay addno longer tells you that you edited a skill when it simply stopped looking. To decide which side moved, quay walks the hub's history looking for the commit your copy came from, and gives up after 200 commits. Exhausting that budget was reported the same way as searching the whole history and finding nothing: differs from harbor (direction unknown, local edits present). On a skill with a long history that sentence was a claim about you, derived from a spent search. The folder-levelquay diffhas always drawn this distinction; the single-file path now does too, and says only what it knows.
Internal
- The reconcile verdict is a closed set of five cases rather than four plus a
boolean carrying three meanings, and a per-file change now holds its own diff
instead of an
Optionthat was only everNonefor unchanged files. No output changed — verified across six hub fixtures through bothquay diffandquay diff --json. quay add's Replace action carries the bytes it writes. It previously took them as a separate argument, which made a zero-byte overwrite representable if the two ever disagreed.
0.15.1 — 2026-07-31
Fixed
-
A hub you cannot reach no longer reads as a hub that deleted your skill.
quay diffandquay add's collision check clone the hub blobless and fetch file contents on demand. The existence probe treated every non-zero exit from git as "this path does not exist at that revision" — but a genuinely absent path and a failed fetch exit with the same code. So an expired token, a network blip or an outage rendered asno longer on the hub (deleted or renamed there), and the reconcile baseline walk quietly skipped commits it could not read, downgrading the verdict to "no commit matches your copy".Existence is now decided from the repository's tree, which is always present locally and needs no network, and only then is content fetched — where a failure is reported as the failure it is.
Only blobless clones were affected, which is every
quay diffagainst a real hub. Nothing was written to disk incorrectly; the verdicts were wrong, not the files.
0.15.0 — 2026-07-31
Changed
-
updateandadd --forcenow notice local files the hub deleted, and ask what to do with them. Both re-materialize a skill from a fresh fetch, and both used to silently carry forward every local file the new version did not contain, forever. That preserved the notes you wrote — and also resurrected every file the hub had removed, soquay diffreported it as local-only for the life of the install. The default is still to keep everything; nothing is deleted without a flag or an explicit answer.quay cannot tell the two apart on its own: the lockfile records a content hash, not a file list, so nothing on disk says what the previous install put there. So it asks. Files the new version lacks are listed, and you can keep them, delete them, pick individually, or keep these and every remaining skill's without being asked again.
Without a terminal — CI, a pipe, or
--json— nothing is deleted. The files are kept and a note says so, which means no existing script changes behaviour on upgrade.--keep-extraand--delete-extradecide it outright and work unattended;--delete-extranames on stderr every file it removed, so an unattended run still leaves a record of it.Dotfiles, dot-directories and symlinks are never offered and never deleted. They are outside the set quay manages, and one of them —
.quay-mirror— is what marks a copy mirror as quay-managed.
Fixed
- A symlink inside an installed skill no longer gets flattened into a plain
copy on
update/add --force. Carrying an extra file forward decided whether to recurse into it by asking if it was a directory, which follows symlinks — so a symlink to a file was silently replaced by a byte-for-byte copy of its target, and a symlink to a directory was worse: the target's contents were recursed into and copied as though they belonged to the skill. Both are now recreated as the symlink they were. On Windows, where recreating a symlink needs Developer Mode or elevation that an ordinary user is unlikely to have, the update warns and carries on rather than failing: a link to a file falls back to copying its contents, and a link to a directory is skipped, since copying a directory's contents in is the very thing this fixes.
0.14.1 — 2026-07-27
Fixed
push --bumpno longer rewrites your frontmatter. It re-emitted the whole block from the parsed manifest, so the hub copy kept only the fields quay models:license: MITcame back aslicense: null, andcompatibility,metadataandallowed-toolswere dropped outright.allowed-toolsis the only machine-readable statement of what a skill may do, and the hub copy is what everyone else installs. The bump now rewrites theversion:scalar and passes every other byte through, CRLF frontmatter included.
0.14.0 — 2026-07-27
Added
-
quay diff <skill>— read-only report of how a locally installed skill differs from the hub's copy.quay outdatedsays that something differs; this says what, file by file, with a verdict derived from hub history rather than from version numbers (hub is ahead by N commits,your copy is ahead,no longer on the hub, …). Supports--remoteand--json.It compares the whole skill directory. The reconcile engine that backs
quay add's collision prompt only ever looked atSKILL.md, so a skill whosereferences/orscripts/moved on the hub reported as identical. Diffs are pull-oriented:+is what the hub would give you.
Fixed
-
quay outdatedno longer misses a hub edit that shipped without a version bump. Frontmatter skills were compared by semver alone, so a hub maintainer who fixed a typo or editedreferences/without touchingversionleft every consumer reading "everything up to date" on a stale copy. Content is now compared for every skill format and reported as its own row —differs from hub at <version>— alongside the existinglocal -> <version>upgrade rows.--jsonand thequay_outdatedMCP tool gain acontent_driftfield so the two reasons can be told apart.Drift is direction-neutral: two hashes prove the bytes differ, not which side changed them.
quay updatestill acts on version upgrades only, so drift rows point atquay add <name> --force.Line endings are normalized before comparing, so a Windows checkout (
core.autocrlf) does not report every installed skill as drifted. The publishedcontent_hashis unchanged — no registry needs regenerating.
0.13.4 — 2026-07-23
Security
registry.jsonfile paths are validated before anything is fetched. The registry is downloaded from the remote hub, and itsfileslist went straight into a path join unchecked — an entry like"../../../.ssh/authorized_keys"wrote there, and an absolute path escaped the skill directory entirely. Absolute paths,..components and Windows drive/UNC prefixes are now rejected. If you install from a hub you do not control, update.
Fixed
- Windows: frontmatter is parsed in files with CRLF line endings. Git's
default
core.autocrlfrewrites line endings on checkout, so on Windows every frontmatter skill silently degraded to "freestyle" — losing its name, description and version, and listing asunversioned. - Windows:
--forcecan replace a symlinked mirror. Unlinking usedremove_file, which fails on the directory symlinks and junctions that mirrors are on Windows, soquay link --force,quay add --forceandquay agents link --forcecould not replace an existing mirror. - Dot-prefixed directories are never treated as skills. A staging directory
left behind by an interrupted
quay addcould appear inquay listas a skill named.tmpAbCdEf, andquay linkwould mirror it into every tool directory.
Internal
- CI runs
cargo fmt,cargo clippyand the test suite on every pull request, on Linux and Windows. The suite had never compiled on Windows, which is how the two bugs above went unnoticed. - The Rust toolchain is pinned in
rust-toolchain.tomlat the repo root, so a new Rust release cannot turn CI red on its own and contributors resolve the same compiler CI uses.
0.13.3 was tagged but never published — the release build failed — and its changes are included here.
0.13.2 — 2026-07-23
Fixed
quay addno longer leaves a partial skill behind when a fetch fails. Files were written directly into.agents/skills/<name>/as they downloaded, so a network failure part-way through stranded a half-installed skill that then blocked its own retry withAlreadyExists. The fetch now stages in a temporary directory and is renamed into place only once every file has landed.- A failed
--forceinstall no longer destroys the existing skill. The previous copy is moved aside and restored if the replace fails, rather than deleted up front. quay updatepreserves files that are not in the skill manifest — local notes, files dropped upstream — matching the previous overwrite-in-place behaviour.
0.13.1 — 2026-07-23
Fixed
quay linkreported mirrors it had just created as no-ops when the interactive adopt opt-in triggered a second reconcile pass. The actions from the first pass are now kept.
0.13.0 — 2026-07-23
Added
- Mirror reconcile.
quay linkscans all known tool directories on disk (.agents/.claude/.codex/.cursor), not just the ones in[install].mirrors, so a mirror added outside the config is still seen. install.auto_link(opt-in) in.quay/config.toml: adopt an unmanaged tool directory that is byte-identical to canonical, converting it to a managed mirror. Asked once interactively and remembered; non-interactive runs (--json, CI) never adopt.
Changed
quay linkrefuses to overwrite a mirror whose content diverged from canonical. Copy-strategy mirrors were previously re-materialised unconditionally and hand edits were lost silently. Pass--forceto discard the mirror edit, or copy it into the canonical skill first.quay link checkis read-only — it detects drift but never creates or overwrites.
0.2.0
Removed
quay sync— skills are tracked by git history; commit your.agents/skills/changes normally.quay create— writeSKILL.mddirectly, with your editor, an AI agent, or any tool.skills.lock.jsonis no longer written. If a legacy lockfile exists, quay prints a one-time notice and instructions to delete it.
Changed
quay addJSON output no longer returnsversion/remotefields — those came from the lockfile.
Use with AI agents (MCP)
quay ships an MCP server. Run quay mcp and it speaks the Model Context Protocol over stdio, exposing the skill-registry operations as structured tools an AI agent calls directly — returning JSON instead of the human table, so the agent reasons over typed results rather than scraping stdout.
It is cross-client: one server, every MCP client (Claude Code, Codex, Cursor, VS Code, opencode, Devin, …).
Quick start
Register quay with your client, then the 12 quay_* tools appear in-session.
# Claude Code — one command:
quay mcp install claude # prints: claude mcp add -s user quay -- quay mcp
# Anything else — print the config snippet and paste it where it says:
quay mcp install codex
quay mcp install cursor
quay mcp install vscode
quay mcp install opencode
quay mcp install devin
quay mcp install generic # universal stdio shape for any other client
quay mcp install <client> only prints the snippet (and where it goes) — it never edits your client's files.
Tools
Each tool carries MCP annotations so the client decides what to auto-run versus confirm. The annotation mirrors blast radius — the agent can't silently publish.
| Tool | Does | Annotation |
|---|---|---|
quay_search | search hubs for skills | read-only, network |
quay_info | metadata for one skill | read-only, network |
quay_list | skills installed here (canonical) | read-only |
quay_outdated | installed skills with newer hub versions | read-only, network |
quay_scan | every SKILL.md on disk (all mirrors) | read-only |
quay_validate | check a skill's frontmatter | read-only |
quay_add | install a skill | write, network |
quay_link | mirror into .claude/, .codex/, … | write |
quay_update | pull newer version | write, network |
quay_remove | uninstall a skill | write, destructive |
quay_push | publish a skill (opens a PR / direct push) | write, network, outward |
quay_remote | add a hub remote to project config | write, network |
quay_add is the workhorse: an agent that hits a capability gap mid-task can search → add → the skill is live in the session.
How it works
- The server links
quay-coredirectly (same code the CLI uses) — no subprocess, no parsing. - It runs in the client's working directory. Tools operate on that project's
.quay/config.toml.search/add/outdated/pushneed remotes configured there — seeremoteandconfig.toml. - Transport is stdio. No ports, no auth — it inherits your existing
gitcredentials for hub access. - Confirmation is the client's job (driven by the annotations above); the server never prompts.
Per-client config shapes
quay mcp install emits the right shape per client. They differ:
| Client | File | Wrapper key | Notes |
|---|---|---|---|
| Claude Code | — | — | claude mcp add -s user quay -- quay mcp (CLI, user scope) |
| Codex | ~/.codex/config.toml | [mcp_servers.quay] | TOML |
| Cursor | .cursor/mcp.json | mcpServers | |
| VS Code | .vscode/mcp.json | servers | requires "type": "stdio"; also code --add-mcp |
| opencode | opencode.json | mcp | "type": "local", command is an array |
| Devin | .devin/config.json | mcpServers | cloud Devin needs quay installed in its VM |
| generic | any | varies | universal stdio shape; check the client's docs for the exact key |
Verify
Without a live client, drive the protocol over stdin:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' | quay mcp
A clean initialize response with "serverInfo":{"name":"quay-mcp",…} on a single stdout line means the server is healthy.
Caveats
quay mcpis a hidden subcommand (not in top-level--help) — it's meant for clients, not interactive use. Running it bare blocks waiting on stdin.- stdout is the protocol channel: the server writes only MCP frames there; diagnostics go to stderr.
quay_pushis annotated outward — a well-behaved client always asks before it opens a PR.
Author your first skill
End-to-end walkthrough: write a small skill, push it to a hub, install it from a different project.
Pre-reqs: quay on PATH, a profile already configured (quay profile add -i if not).
1. Pick a tiny example
We'll author a skill that teaches an agent to apply a one-rule lint fix: "use early returns instead of nested else."
mkdir -p ~/code/origin-project/.agents/skills/early-returns
cd ~/code/origin-project
$EDITOR .agents/skills/early-returns/SKILL.md
Paste:
---
name: early-returns
description: Refactor nested `else` chains into early returns.
version: 0.1.0
tags: [refactor, code-style]
---
# Early returns
When you see code shaped like:
```js
if (cond) {
doA();
} else {
doB();
}
…and doA is short, prefer an early return:
if (cond) {
doA();
return;
}
doB();
Apply to JavaScript, TypeScript, and Rust. Skip Python (no early-return idiom in match-case).
## 2. Validate offline
```sh
quay validate early-returns
Expect OK (or warnings on stderr; exit 0 in soft mode). Fix anything flagged before pushing.
3. Push
quay push early-returns
What happens:
quayclones your default-profile hub into a temp dir.- Copies
SKILL.mdtoskills/early-returns/SKILL.md. - Updates the hub's
registry.jsonwith name, description, version, tags. - Commits.
- Per your remote's
push_mode: opens a PR (default) or pushes the branch directly. - Prints the PR URL or commit SHA.
- Appends an entry to
~/.config/quay/push-log.json.
If push_mode = pr, merge the PR before continuing.
4. Consume from a different project
In a fresh shell, fresh directory:
mkdir -p ~/code/consumer-project
cd ~/code/consumer-project
git init # optional; quay does not require git
quay add early-returns
The skill lands at .agents/skills/early-returns/SKILL.md. Mirror dirs (.claude/, .cursor/, …) get copies based on your profile's [install].mirrors.
Confirm:
quay list
quay scan
scan should show early-returns with status installed.
5. What if you edit the local copy?
Edit consumer-project/.agents/skills/early-returns/SKILL.md — add a sentence. Re-scan:
quay scan
Status flips to installed-modified. quay outdated will refuse to overwrite it. To force re-sync:
quay add early-returns --force
Next
- Multi-provider setup — same flow, but a second hub on a different provider.
quay push— full flag reference, including--bump.
Multi-provider setup
Three profiles on one machine: work GitHub, work GitLab, personal Azure DevOps. Switch with one command.
Why three profiles?
Each profile is a bundle of:
- Commit identity — author email used in hub commits.
- Remote(s) — hub URLs plus provider kind and push mode.
- Install settings — mirror dirs.
You want a separate profile per identity so commits to your work hub are tagged with your work email, and personal pushes use your personal email.
1. First profile: work GitHub (wizard)
quay profile add -i
Answer:
- Name:
work-github - Email:
you@corp.example - Hub URL:
git@github.com:my-org/skills-hub.git - Provider:
github(auto-detected) - Push mode:
pr - Activate: yes
Confirm:
quay profile current # → work-github
quay profile show
2. Second profile: work GitLab (explicit flags)
quay profile add work-gitlab \
--email you@corp.example \
--remote main=git@gitlab.com:my-org/skills-hub.git \
--provider gitlab \
--push-mode pr \
--default
--default marks main as the profile's default remote.
3. Third profile: personal Azure DevOps (TOML)
Useful pattern for committable configs. Drop this in ~/code/quay-profiles/personal-azure.toml:
name = "personal-azure"
email = "me@personal.example"
[[remotes]]
name = "main"
url = "https://dev.azure.com/myorg/skills/_git/skills-hub"
provider = "azuredevops"
push_mode = "pr"
default = true
Add it:
quay profile add personal-azure --from-toml ~/code/quay-profiles/personal-azure.toml
Stdin works too — handy for scripted bootstrap:
cat ~/code/quay-profiles/personal-azure.toml | quay profile add personal-azure --from-toml -
4. Switch profiles
Interactive picker:
quay profile use -i
Direct:
quay profile use work-gitlab
quay profile current # → work-gitlab
For a one-shot override without persisting:
quay --profile personal-azure push early-returns
# or
QUAY_PROFILE=personal-azure quay push early-returns
5. Push the same skill to all three
Author once, publish thrice:
quay profile use work-github
quay push early-returns
quay profile use work-gitlab
quay push early-returns
quay profile use personal-azure
quay push early-returns
Each push records to ~/.config/quay/push-log.json with the active profile + remote URL.
6. Verify
quay profile list
Expect three entries with the active one starred. quay search early-returns (run under any profile) returns hits across that profile's remotes.
Next
- Per-provider deep-dives: GitHub, GitLab, Azure DevOps, Bitbucket.
- Recipe: Multi-org profiles.
GitHub
GitHub-specific setup notes. Covers github.com and GitHub Enterprise (githubenterprise).
Auth
quay clones over SSH or HTTPS — whatever URL you give it. SSH is simplest:
- Generate a key:
ssh-keygen -t ed25519 -C "you@example.com". - Add the public key at https://github.com/settings/keys.
- Test:
ssh -T git@github.com(expect a "successfully authenticated" message).
HTTPS works too if you have git config --global credential.helper wired up (Keychain on macOS, libsecret on Linux, manager-core on Windows).
CLI binding: gh
For PR creation, quay shells out to gh. Install:
brew install gh # macOS
sudo apt install gh # Debian/Ubuntu (or via official .deb)
Authenticate once:
gh auth login
Pick HTTPS or SSH (matching your clone preference). Without gh, quay push in PR mode falls back to printing a compare URL — you'll need to open the PR in the browser manually.
URL formats
| Form | Use |
|---|---|
git@github.com:org/repo.git | SSH. Auto-detected as github. |
https://github.com/org/repo.git | HTTPS. Auto-detected as github. |
git@ghe.example.com:org/repo.git | GitHub Enterprise. Pass --provider githubenterprise. |
Add the remote
quay remote add work git@github.com:my-org/skills-hub.git --provider github --default
quay remote test work # connectivity probe
Branch policy gotchas
- If
mainis protected (required reviews, status checks),push_mode = "direct"will fail atgit push origin main. Usepush_mode = "pr"(the default) or pass--push-mode pr. - The PR is opened against the default branch, not necessarily
main. quay reads the remote'sHEADref after clone. - The PR branch is named
quay/<skill-name>-<version>. Pruned automatically on merge if your repo has "Automatically delete head branches" enabled.
Enterprise
GitHub Enterprise Server uses the same gh CLI, but you must authenticate against the EE host:
gh auth login --hostname ghe.example.com
Set the provider explicitly:
quay remote add ee git@ghe.example.com:platform/skills.git --provider githubenterprise --default
Common errors
| Symptom | Fix |
|---|---|
gh: command not found (in PR mode) | Install gh, or accept compare-URL fallback. |
Permission denied (publickey) | SSH agent not running / key not added. ssh-add ~/.ssh/id_ed25519. |
403 Resource not accessible (from gh) | PAT scope missing. Re-run gh auth login and grant repo. |
Stale registry.json after push | CDN cache; wait ~60s or quay rebuild-registry. |
Next
quay push— flag reference.- Recipe: Direct vs PR push.
GitLab
GitLab-specific setup. Works for gitlab.com and self-hosted GitLab CE/EE.
Auth
SSH is the typical path:
ssh-keygen -t ed25519 -C "you@example.com".- Paste the public key at https://gitlab.com/-/user_settings/ssh_keys.
- Test:
ssh -T git@gitlab.com(expect "Welcome to GitLab, @you!").
HTTPS works with a personal access token — generate one at https://gitlab.com/-/user_settings/personal_access_tokens with write_repository scope and configure your credential helper.
CLI binding: glab
For MR (merge request) creation, quay shells out to glab. Install:
brew install glab
Authenticate:
glab auth login
Pick the right host (gitlab.com or your self-hosted instance) and method (HTTPS-token or SSH). Without glab, PR mode falls back to a compare URL.
URL formats
| Form | Use |
|---|---|
git@gitlab.com:group/repo.git | SSH. Auto-detected as gitlab. |
https://gitlab.com/group/repo.git | HTTPS. Auto-detected as gitlab. |
git@git.corp.example:platform/skills.git | Self-hosted. Pass --provider gitlab to be explicit. |
Add the remote
quay remote add work git@gitlab.com:my-group/skills-hub.git --provider gitlab --default
quay remote test work
Default branch quirks
- Newer projects default to
main; older ones may still be onmaster. quay readsHEADafter clone, so either works without configuration. Default branchsettings on the project page determine which branch your MR targets. Change it there if needed.
Branch policy gotchas
- "Protect default branch" is the GitLab equivalent of GitHub's branch protection.
push_mode = "direct"will fail withremote: GitLab: You are not allowed to push code to protected branches. - "Approval rules" do not block
glab mr create; they block the merge. Your MR is opened successfully, then awaits approvals. - Squash settings come from project config; quay does not override them.
Self-hosted
The provider name is gitlab regardless of where the instance lives — the URL is what matters. Authenticate glab against the right host:
glab auth login --hostname git.corp.example
Common errors
| Symptom | Fix |
|---|---|
glab: command not found | Install glab or accept compare-URL fallback. |
error: failed to push some refs to 'gitlab.com' | Wrong key in agent, or branch protected. Check ssh-add -l and project settings. |
401 Unauthorized from glab | Token expired or scope missing. glab auth login again. |
| MR opens but immediately closes | Project has "Auto-close MR" tied to default-branch matches; check project settings → Merge requests. |
Next
Azure DevOps
Azure DevOps Repos (dev.azure.com) and Azure DevOps Server (on-prem).
Auth
Two options, both common:
SSH — generate a key, paste at User settings → SSH public keys in Azure DevOps. Test:
ssh -T git@ssh.dev.azure.com
PAT over HTTPS — generate a Personal Access Token with Code (Read & write) scope. Use it as the password when prompted; cache via your credential helper.
CLI binding: az repos
quay uses az repos pr create for PR mode. Install the Azure CLI:
brew install azure-cli # macOS
# or: curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
az extension add --name azure-devops
Sign in:
az login
az devops configure --defaults organization=https://dev.azure.com/myorg project=skills
Without az, PR mode falls back to printing a compare URL.
URL formats
Azure DevOps URLs are long. Two equivalents quay accepts:
| Form | Use |
|---|---|
https://dev.azure.com/<org>/<project>/_git/<repo> | HTTPS. Auto-detected as azuredevops. |
git@ssh.dev.azure.com:v3/<org>/<project>/<repo> | SSH. Auto-detected as azuredevops. |
Add the remote
quay remote add work https://dev.azure.com/myorg/skills/_git/skills-hub --provider azuredevops --default
quay remote test work
Branch policy gotchas
Azure DevOps "Branch policies" are powerful and strict:
- Minimum reviewers — direct push to a policied branch fails. Use
push_mode = "pr". - Build validation — PR cannot complete until pipeline passes. quay's job ends at PR-create; you wait for the build.
- Required reviewers — auto-assigned, no quay involvement.
For low-friction internal hubs, exempt the hub repo from branch policies (Repos → Branches → policies) and use push_mode = "direct".
Default branch
Reads HEAD from the remote after clone. New repos default to main; older ones may be master. Set the default at Repos → Branches.
Common errors
| Symptom | Fix |
|---|---|
az: command not found | Install Azure CLI + azure-devops extension. |
TF401019 (no access) | PAT scope wrong; regenerate with Code (Read & write). |
RemoteRejection: pre-receive hook | Branch policy. Use --push-mode pr. |
pull request creation failed from az | az devops configure defaults not set, or extension missing. Re-run setup. |
Server (on-prem)
Same provider name (azuredevops). Authenticate az against the on-prem URL:
az devops configure --defaults organization=https://tfs.corp.example/<collection> project=skills
Next
Bitbucket
Bitbucket Cloud (bitbucket.org) and Bitbucket Data Center (server).
The Bitbucket gotcha
Bitbucket has no first-party CLI with the breadth of gh / glab / az repos. quay does not shell out to anything. Consequence: PR mode always falls back to a compare URL. You open the PR in the browser manually.
If you want a hands-off flow, use push_mode = "direct" against an unprotected hub repo.
Auth
SSH:
ssh-keygen -t ed25519 -C "you@example.com".- Paste the public key at https://bitbucket.org/account/settings/ssh-keys/.
- Test:
ssh -T git@bitbucket.org.
HTTPS uses an App Password, not your account password:
- https://bitbucket.org/account/settings/app-passwords/ → Create app password.
- Grant
Repositories: Read, Write. - Use it as the password when prompted; cache via credential helper.
URL formats
| Form | Use |
|---|---|
git@bitbucket.org:workspace/repo.git | SSH. Auto-detected as bitbucket. |
https://bitbucket.org/workspace/repo.git | HTTPS. Auto-detected as bitbucket. |
Add the remote
quay remote add work git@bitbucket.org:my-workspace/skills-hub.git --provider bitbucket --default
quay remote test work
Direct push
quay remote add work git@bitbucket.org:my-workspace/skills-hub.git \
--provider bitbucket --push-mode direct --default
quay push early-returns # commit lands on default branch immediately
PR mode (compare-URL fallback)
quay remote add work git@bitbucket.org:my-workspace/skills-hub.git \
--provider bitbucket --push-mode pr --default
quay push early-returns
# Output: branch pushed, open this URL to create the PR:
# https://bitbucket.org/my-workspace/skills-hub/pull-requests/new?source=...&dest=...
quay pushes the branch and prints the compare URL. Click → fill out the PR description → submit.
Branch policy gotchas
- Bitbucket "Branch restrictions" block direct pushes. Use
push_mode = "pr". - "Default reviewers" auto-attach on the compare-URL form; you don't need to add them manually.
- Bitbucket squash-on-merge is per-repo. quay does not override it.
Data Center (server)
Same provider name (bitbucket). URLs differ (git@stash.corp.example:scm/proj/repo.git); detection by host substring is approximate, so pass --provider bitbucket explicitly.
Common errors
| Symptom | Fix |
|---|---|
Permission denied (publickey) | SSH key not added at account-settings → SSH keys. |
401 over HTTPS | Using account password instead of App Password. Generate one. |
pre-receive hook declined on direct push | Branch restriction. Use --push-mode pr. |
Next
- Direct vs PR push — when each is appropriate.
quay remote test— verify connectivity before pushing.
CLI overview
quay is a single binary with fifteen subcommands. Every subcommand respects four global options:
| Flag | Effect |
|---|---|
--project <PATH> | Treat <PATH> as the project root instead of cwd. |
--user-config <PATH> | Override user config (default: ~/.config/quay/config.toml). |
--profile <NAME> | Override active profile for this invocation. QUAY_PROFILE env var works too. |
--json | Emit machine-readable JSON on stdout instead of a human table. |
Subcommand groups
Project setup
init— drop a.quay/config.tomlskeleton into the current project.remote— add / list / test / remove configured hub remotes.profile— manage user profiles (work + personal on one machine).
Consume
add— install a skill from a hub.list— list currently installed skills (no dedicated page; covered briefly below).info— show metadata for a skill without installing.search— free-text search across configured remotes.outdated— list installed skills that differ from the hub.diff— show what differs, file by file, for one skill.update— pull newer versions over installed skills.remove— uninstall a skill (optionally--everywhere).link— manage mirror dirs (.claude/,.cursor/, …).
Author / publish
scan— discover local skills and their sync status.validate— check frontmatter offline.push— publish a skill to a hub.rebuild-registry— regenerate hubregistry.jsonfrom disk.
quay list
Prints every installed skill in the current project plus its version and source remote. Common flags only (--profile, --user-config, --project, --json). Use it as a sanity check after quay add, or to feed xargs in scripts:
quay list --json | jq -r '.[].name' | xargs -n1 quay info
Exit codes
See Exit codes for the full table. Quick reference:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic command failure |
| 2 | clap parse error (bad flags / args) |
--json example
quay list --json | jq '.[] | {name, version}'
Use this for piping into other tools. Human-table output is not stable; the JSON shape is.
Top-level help
Skill registry CLI
Usage: quay [OPTIONS] <COMMAND>
Commands:
init Initialize quay in the current project
remote Manage configured hub remotes
add Install a skill from a configured remote
list List installed skills
remove Remove a previously installed skill
info Show metadata for a skill (without installing)
search Search across configured remotes
diff Show how a locally installed skill differs from the hub's copy
outdated List installed skills that have newer versions available
update Update installed skills to the latest available version
scan Discover local skills under `.agents/skills/` and report their sync status
validate Validate a local skill's frontmatter (offline, no network)
push Push a local skill to a hub via PR (or directly, if --push-mode=direct or the remote's TOML says so)
profile Manage user profiles (multi-org identities + remote bundles)
rebuild-registry Rebuild a hub's `registry.json` from disk truth and push it back
link Apply or verify mirrors from `[install].mirrors` config
help Print this message or the help of the given subcommand(s)
quay add
Install a skill from a configured remote.
Usage
quay add [OPTIONS] [SKILL]
SKILL is the skill name as stored in the hub's registry.json (typically the directory name under skills/). Omit it with -i to pick interactively.
Examples
quay add hello # install one
quay add -i # pick several from a checkbox list
quay add hello --remote work # explicit remote
quay add hello --force # overwrite local edits
Inside a TTY, omitting SKILL and not passing -i triggers the picker automatically (default-interactive). Pipe < /dev/null to keep the prompt suppressed in scripts.
Flags
| Flag | Effect |
|---|---|
--remote <NAME> | Use a specific remote instead of the profile default. |
--force | Overwrite the skill even if it already exists locally. |
-i, --interactive | Open the checkbox picker. Mutually exclusive with SKILL. |
--keep-extra | With --force, keep local files the new version does not contain, without prompting or printing the note. |
--delete-extra | With --force, delete local files the new version does not contain, without prompting. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay update— refresh an already installed skill to the latest hub version.adderrors with "already installed" unless--force.quay info— preview metadata without writing files.
Caveats
- A skill that fails
quay validateon the hub side will still install, but you'll see warnings. - Mirror dirs (
.claude/skills/,.cursor/rules/, …) are populated automatically based on your profile's[install].mirrors. --forceoverwrites every file the fetched version contains, without confirmation — local edits toSKILL.mdare lost. Files it doesn't contain are a separate question; see below. Usequay outdatedfirst to see drift status.
--force and local files
--force overwrites an existing install. Files it finds that the fetched version
does not contain are handled exactly as on quay update — you are asked, and
nothing is deleted unattended. --keep-extra and --delete-extra work here too.
See update.
--help
Install a skill from a configured remote
Usage: quay add [OPTIONS] [SKILL]
Arguments:
[SKILL] Skill name(s) to install. Omit when using --interactive (-i)
Options:
--project <PROJECT> Project root (defaults to current directory)
--remote <REMOTE>
--force Overwrite the skill if it already exists locally
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
-i, --interactive Open an interactive checkbox list to pick skills to install. Mutually exclusive with the positional skill argument
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
--no-diff Suppress the diff body on a collision (still prints the verdict line)
--keep-extra Keep local files the new version does not contain (the default when there is no terminal). Only meaningful with --force
--delete-extra Delete local files the new version does not contain. Only meaningful with --force
-h, --help Print help
quay push
Publish a local skill to a hub.
Usage
quay push [OPTIONS] [SKILL]
quay push clones the configured hub into a temp dir, drops your skill at skills/<name>/SKILL.md, updates the hub's registry.json, commits, and then either opens a PR (default) or pushes to the default branch directly (--push-mode direct or per-remote setting).
Examples
quay push hello # default profile, default remote, push_mode from remote config
quay push hello --remote work # explicit remote
quay push hello --push-mode direct # bypass remote setting, push straight to main
quay push hello --bump patch # 0.1.0 → 0.1.1 before pushing
quay push hello --bump minor
quay push hello --bump major
quay push -i # interactive checkbox picker
After success, the PR URL (or commit SHA, for direct mode) is printed. The push is also logged to ~/.config/quay/push-log.json.
Flags
| Flag | Effect |
|---|---|
--remote <NAME> | Target a specific remote. |
--bump <KIND> | Bump version in frontmatter: patch / minor / major / as-written (default). |
--push-mode <pr|direct> | Override the remote's push_mode for this invocation. |
-i, --interactive | Open the checkbox picker. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay rebuild-registry— regenerate a corrupted/missingregistry.json. Use after manual hub surgery, not as a normal publish step.quay validatefirst — catches frontmatter mistakes offline before incurring a hub clone.
Caveats
--bumprequires Frontmatter format. SlashCommand / Freestyle skills get a clean error if you pass--bumpagainst them.- Direct mode against a branch-protected default branch will fail at
git push origin main. The error hint suggests--push-mode pror settingpush_mode = "pr"in the remote config. - The first push to an empty hub seeds
registry.jsonautomatically — no manual setup needed. - Bitbucket has no CLI client. PR mode falls back to printing a compare URL; you must open the PR in the browser manually.
--help
Push a local skill to a hub via PR (or directly, if --push-mode=direct or the remote's TOML says so)
Usage: quay push [OPTIONS] [SKILL]
Arguments:
[SKILL] Skill name to push. Omit when using --interactive (-i)
Options:
--project <PROJECT> Project root (defaults to current directory)
--remote <REMOTE>
--bump <BUMP> Bump kind: patch | minor | major | as-written. Default: as-written [default: as-written]
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
--push-mode <PUSH_MODE> Override the remote's `push_mode` setting for this invocation. Values: pr, direct [possible values: pr, direct]
-i, --interactive Open an interactive checkbox list of local skills to push. Mutually exclusive with the positional skill argument
--json Output JSON instead of human-readable text
-h, --help Print help
quay remove
Uninstall a previously installed skill.
Usage
quay remove [OPTIONS] [SKILL]
Default: removes only the local copy under .agents/skills/<name>/ and any mirror dirs. The hub is untouched. Pass --everywhere to also delete the skill from every configured remote that publishes it.
Examples
quay remove hello # local only
quay remove hello --everywhere # also drop from every hub
quay remove -i # interactive picker
Flags
| Flag | Effect |
|---|---|
--everywhere | Also commit a deletion to every remote publishing this skill (respects per-remote push_mode). |
-i, --interactive | Pick from installed skills. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay update— to refresh, not uninstall.quay linkremove <path>— remove a mirror directory from the project config, not a skill.
Caveats
--everywheredeletes from the remote without a confirmation prompt. Combined withpush_mode = "direct"it's a destructive one-shot. Use PR mode if you want a review gate.- Removing locally does not edit
~/.config/quay/push-log.json— the push history is preserved for audit. - If a skill was installed via mirror dir only (rare),
removestill cleans up the canonical.agents/skills/<name>/path.
--help
Remove a previously installed skill
Usage: quay remove [OPTIONS] [SKILL]
Arguments:
[SKILL] Skill name to remove. Omit when using --interactive (-i)
Options:
--everywhere Also push a deletion commit to every remote that publishes the skill
--project <PROJECT> Project root (defaults to current directory)
-i, --interactive Open an interactive checkbox list to pick skills to remove. Mutually exclusive with the positional skill argument
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
-h, --help Print help
quay update
Update installed skills to the latest hub version.
Usage
quay update [OPTIONS] [SKILL]
Omit SKILL to update every installed skill that has a newer version on the hub. Inside a TTY this auto-opens a checkbox picker of outdated skills; pass --all to bypass the picker, or --dry-run to preview.
Examples
quay update # TTY: opens picker of outdated. Non-TTY: updates all.
quay update hello # just this one
quay update --all # all outdated, no picker
quay update --dry-run # show what would change
quay update -i # explicit picker
Flags
| Flag | Effect |
|---|---|
--dry-run | Show what would change without writing. |
-i, --interactive | Open the picker. |
--all | Skip the TTY auto-picker; update everything outdated. |
--keep-extra | Keep local files the new version does not contain, without prompting or printing the note. |
--delete-extra | Delete local files the new version does not contain, without prompting. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay outdated— list-only sibling; identical detection logic without writing anything.quay add<skill> --force— overwrites local drift even when versions match.
Caveats
- "Outdated" uses semver comparison on the frontmatter
versionfield. Skills without frontmatter (SlashCommand / Freestyle) never show as outdated by version — they're compared by SHA-on-fly instead. updateoverwrites the installed copy with the hub's. Edits you made to files the hub also ships are replaced; checkquay diff<skill>first if you may have local changes. Files the hub doesn't ship are handled as below.- Copy-strategy mirrors are refreshed after each update, so they don't keep the old version or a file the update deleted. A mirror you changed by hand — any file, dotfiles included — is left alone with a warning (on stderr, under
--jsontoo);quay link --forceoverwrites it. A copy mirror made before 0.16 is picked up automatically the first time quay sees it matching canonical; one that already differs needs a singlequay link --force. - If a skill fails partway through a multi-skill run, the skills already updated are still reported (and in
--json, with theirdeleted_extras) before the error. - Cancelling a prompt (Ctrl-C) stops the run and exits non-zero, so a wrapping script does not mistake a partial update for success.
--dry-rundoes not query CLI tools (gh/glab); pure diff against hub clone.
Local files the new version doesn't have
A skill is a directory, and yours may hold files the hub's copy does not — notes you wrote, or a file the hub has since deleted. quay cannot tell those two apart: the lockfile records a content hash, not a file list, so nothing on disk says what the previous install put there.
So it asks. When an update finds files the new version does not contain, it lists them and offers to keep them, delete them, pick individually, or keep these and every remaining skill's without asking again.
csv-parse: 2 files not in the new version
refs/legacy.md
notes.md
Without a terminal — CI, a pipe, or --json — nothing is deleted. quay keeps the
files and says so:
note: csv-parse — kept 2 files not in the new version
(refs/legacy.md, notes.md). Pass --delete-extra to remove them.
| Flag | Effect |
|---|---|
--keep-extra | Keep them, no prompt, and no note either — this is the only way to suppress it. |
--delete-extra | Delete them, no prompt. Works without a terminal, and names on stderr what it removed. |
note: csv-parse — deleting 2 files not in the new version
(refs/legacy.md, notes.md). Pass --keep-extra to keep them.
Note that the bare unattended default (no flag, no terminal) also keeps everything —
--keep-extra only changes whether the note is printed.
Dotfiles, dot-directories and symlinks are never offered and never deleted. They
are outside the set quay manages, and one of them — .quay-mirror — is what marks
a copy mirror as quay-managed.
The "not in the new version" set is derived from the hub's registry.json
files list for that skill, not from the actual fetched tree. A hub whose
registry has gone stale — edited by hand without a quay rebuild-registry —
can therefore list a file as missing when it still exists upstream. An
interactive prompt shows the filename, so a human can recognize and keep it;
--delete-extra in an unattended run has no such backstop and will delete it —
it does name every file it removed on stderr, so a CI log keeps the record.
With --json, each updated skill also carries deleted_extras — the files the
update removed, [] when it kept everything. (Picker runs — -i, or a bare
quay update in a terminal — print no JSON; pass a skill name or --all.)
[{ "name": "csv-parse", "available": "2.0.0", "deleted_extras": ["refs/legacy.md"], "...": "..." }]
Files whose names are not valid UTF-8 are never offered and never deleted, like dotfiles: quay cannot name them exactly, so it does not manage them.
--help
Update installed skills to the latest available version
Usage: quay update [OPTIONS] [SKILL]
Arguments:
[SKILL] Update only this skill; if omitted, updates every installed skill
Options:
--dry-run Show what would change without writing to disk
--project <PROJECT> Project root (defaults to current directory)
-i, --interactive Open an interactive checkbox list of outdated skills to update. Mutually exclusive with the positional skill argument
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--all Update every installed skill without opening the picker, even in a terminal. Explicit bypass for the TTY auto-trigger
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
--keep-extra Keep local files the new version does not contain (the default when there is no terminal)
--delete-extra Delete local files the new version does not contain
-h, --help Print help
quay search
Free-text search across configured remotes.
Usage
quay search [OPTIONS] <QUERY>
Matches <QUERY> case-insensitively against skill name and description in every reachable hub's registry.json. Add --tag to narrow by tag.
Examples
quay search lint
quay search "react component" --tag frontend
quay search foo --remote personal
quay search --json deploy | jq '.[].name'
Flags
| Flag | Effect |
|---|---|
--remote <NAME> | Search only one remote. Default: all configured. |
--tag <TAG> | Restrict to skills whose tags include <TAG>. Repeat not supported. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay info— when you already know the name, get full metadata.quay scan— local-side equivalent; lists your skills, not the hubs'.
Caveats
- Search hits a cached
registry.jsonper remote. If the hub was just pushed to seconds ago, you may get a stale result —?_cbcache-busting in the fetcher mitigates this, but five-minute CDN tail can still bite. Retry after a minute. - A hub with a malformed
registry.jsonis skipped silently (logged to stderr); the rest of the search continues. - Empty results print a clean "no results" line and exit 0.
--help
Search across configured remotes
Usage: quay search [OPTIONS] <QUERY>
Arguments:
<QUERY> Free-text query matched against skill name + description (case-insensitive)
Options:
--project <PROJECT> Project root (defaults to current directory)
--remote <REMOTE>
--tag <TAG>
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
-h, --help Print help
quay outdated
List installed skills that have a newer version available on the hub.
Usage
quay outdated [OPTIONS]
Read-only sibling of quay update. Detects two things:
- Version upgrade — the hub's frontmatter
versionis semver-greater than the installed copy's. Reported aslocal -> <version>. - Content drift — the local bytes differ from the hub's published
content_hash, whatever the versions say. Reported asdiffers from hub at <version>.
Content drift matters because bumping version on push is a convention, not something quay enforces. A hub maintainer who fixes a typo or edits references/ has no reason to touch version, and a semver-only comparison would call your stale copy up to date. Hand-written skills (SlashCommand, Freestyle) have no semver at all, so drift is the only signal they ever produce.
Drift is direction-neutral: two hashes can prove the bytes differ, not which side changed them. Either the hub was edited or you were. quay update acts on version upgrades only, so a drift row is taken with:
quay add <name> --force
Examples
quay outdated
quay outdated --json | jq '.[].name'
Flags
| Flag | Effect |
|---|---|
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay update--dry-run— overlapping but narrower:updateacts on version upgrades only, so it will not offer to resolve a content-drift row. Useoutdatedwhen you want a non-interactive read-only report (CI, status dashboards).quay scan— broader local audit; reports all four statuses (local,installed,installed-modified,pushed-local).
Caveats
- Version comparison only applies to Frontmatter skills. Other formats have no semver and are compared by content hash alone.
- A hub whose
registry.jsonpredates content-hash indexing publishes nocontent_hash. Drift cannot be computed there, and is never reported — a missing hash is not evidence of a match. - Line endings are normalized before comparing, so a Windows checkout (
core.autocrlfrewrites LF to CRLF) does not report every skill as drifted. This normalizes the local side only: a hub that published a hash computed from CRLF bytes still mismatches an LF checkout. - A hub with no
registry.jsonproduces a warning and counts as "everything's current."
--help
List installed skills that have newer versions available
Usage: quay outdated [OPTIONS]
Options:
--project <PROJECT> Project root (defaults to current directory)
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
-h, --help Print help
quay diff
Show how a locally installed skill differs from the hub's copy. Read-only — it never writes to your project.
Usage
quay diff <SKILL> [OPTIONS]
quay outdated tells you that something differs. This tells you what, file by file.
What it compares
The whole skill directory, not just SKILL.md. A hub edit to references/api.md or scripts/run.sh shows up here; comparing only SKILL.md would report such a skill as identical.
The file set matches what quay push sends — dotfiles, dotdirs, symlinks and submodules are excluded on both sides, so neither a .gitkeep nor a symlink living on the hub is reported as a difference quay add could never resolve.
Verdicts
| Verdict | Meaning |
|---|---|
up to date | Your copy and the hub's match (after LF normalization — see below). |
hub is ahead by N commit(s) | Your copy matches an earlier hub commit, and the hub has moved on since. |
your copy is ahead of the hub | The hub's HEAD is an ancestor of the commit your copy matches. |
differs …, direction is unknown | No hub commit matches your bytes. Usually means you edited locally, but a squashed or rewritten hub history looks the same. A separate wording says so when the 50-commit search was cut short rather than exhausted — that is not a conclusion. |
no longer on the hub | The skill directory does not exist at the hub's HEAD — deleted or renamed upstream. |
The verdict comes from history, not from version numbers. A frontmatter version relation is printed as an advisory line when the two sides disagree, and never decides the verdict: a hub can publish new content at an unchanged version, and your copy can carry a higher version than the hub while holding older content.
Reading the diff
Diffs are pull-oriented: + is what the hub would give you, - is what you have now. (quay add's collision prompt renders the opposite direction, because there the question is what you would push.)
Per-file markers:
| Mark | Meaning |
|---|---|
M | Present on both sides, contents differ. |
+ | On the hub only. quay add --force installs it if the hub's registry.json lists it. |
- | Local only. quay add --force keeps these — it preserves files outside the hub's manifest. |
Binary (non-UTF-8) files report byte counts instead of a diff body.
Line endings are normalized to LF on both sides before comparing. git's default core.autocrlf on Windows hands back CRLF at checkout while the hub's blobs hold LF, so a raw byte comparison would mark every file in every skill as modified there. A skill that differs only in line endings therefore reports as up to date.
Examples
quay diff csv-parse
quay diff csv-parse --remote team-hub
quay diff csv-parse --json | jq '.files[] | select(.change != "same") | .path'
When the verdict is hub_newer, the JSON also carries commits_ahead and
last_commit_date — the same facts as the human headline.
Flags
| Flag | Effect |
|---|---|
--remote <name> | Hub to compare against. Defaults to the remote flagged default = true; with no default configured this is required rather than guessed, matching add/push/remove. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay outdated— scans every installed skill and says which ones differ. Use it first; usediffto drill into one.quay add--force— what you run once you have decided to take the hub's copy. It overwrites the files the hub publishes and leaves any extra local files in place, so a-row above survives it.quay scan— local audit only, never contacts a hub.
Caveats
- Clones the hub to read its history (partial clone where the server allows it), so it needs network access and is slower than
outdated. - The history walk is capped at 50 commits touching the skill — tighter than the single-file reconcile's 200, because each candidate commit costs a tree listing plus one read per file. A copy older than that is reported as a cut-short search, not as a local edit.
your copy is ahead of the hubneeds the matching commit to sit off HEAD's ancestry, which only happens after the hub force-pushes or rewinds. A plain local edit reportsdirection is unknowninstead.- Only the canonical copy is compared. If a skill's mirrors have diverged from each other, a warning says so and names the copy that was used.
- The advisory version line reads the hub's version from
registry.json, not from the hub'sSKILL.md, so a stale registry can disagree with the harbor. - The skill must be installed locally and published by the remote; either one missing is an error, not an empty report.
--help
Show how a locally installed skill differs from the hub's copy
Usage: quay diff [OPTIONS] <SKILL>
Arguments:
<SKILL> Installed skill name
Options:
--project <PROJECT> Project root (defaults to current directory)
--remote <REMOTE> Hub to compare against (defaults to the default remote)
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
-h, --help Print help
quay scan
Discover local skills and report their sync status.
Usage
quay scan [OPTIONS]
Walks .agents/skills/ one level deep, classifies every directory it finds, and prints a table. Cross-references the project lockfile and ~/.config/quay/push-log.json to compute per-skill status:
| Status | Meaning |
|---|---|
local | Authored here; never pushed. |
installed | Came from a hub via quay add. |
installed-modified | Came from a hub, but local file has been edited since install. |
pushed-local | Authored here, then pushed (so it exists on a hub too). |
Examples
quay scan
quay scan --json | jq '.[] | select(.status == "installed-modified")'
quay scan --root ./vendor/skills # non-default canonical root
Flags
| Flag | Effect |
|---|---|
--root <PATH> | Override the canonical skills root (default: <project>/.agents/skills). |
--json | JSON output (also accepted as --json global). |
--profile, --user-config, --project | Standard globals. |
When to use this vs …
quay outdated— only reports drift against the hub.scanreports authorship + drift in one pass.quay list— pre-scan command; shows installed skills only, no local-authored entries.
Caveats
- A skill with no recognisable format still appears (status
local, formatFreestyle).scannever errors on weird files; it just classifies. --rootaffects discovery but does not rewrite the project config; if you regularly use a non-default root, set it in.quay/config.tomlinstead.
--help
Discover local skills under `.agents/skills/` and report their sync status
Usage: quay scan [OPTIONS]
Options:
--project <PROJECT> Project root (defaults to current directory)
--root <ROOT> Override the install canonical root (default: `<project>/.agents/skills`)
--json Emit JSON instead of a human table
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
-h, --help Print help
quay validate
Validate a local skill's frontmatter offline.
Usage
quay validate [OPTIONS] <SKILL>
Reads .agents/skills/<SKILL>/SKILL.md and checks:
- Frontmatter parses as YAML.
- Required fields (
name,description) present. namematches the directory.versionis semver-valid (if present).tagsis a string array (if present).
No network calls.
Examples
quay validate hello
quay validate hello --strict
quay validate hello --json
Flags
| Flag | Effect |
|---|---|
--strict | Treat missing/required-field issues as errors (exit 1). Default: warns to stderr, exits 0. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
- Run before
quay push— catches mistakes without paying the hub-clone cost. quay scan— gives status per skill but does not deep-validate frontmatter.
Caveats
- Default mode is soft: even a missing frontmatter block exits 0 with a warning.
--strictis what you want in CI. - SlashCommand and Freestyle skills produce a warning (no frontmatter) by design;
--strictupgrades that to an error. - Validation is purely structural. It does not check whether the content of the skill makes sense for any specific agent runtime.
--help
Validate a local skill's frontmatter (offline, no network)
Usage: quay validate [OPTIONS] <SKILL>
Arguments:
<SKILL>
Options:
--project <PROJECT> Project root (defaults to current directory)
--strict Treat missing frontmatter or required fields as errors (exit 1). Default: prints warnings to stderr, exits 0 (lenient/soft mode)
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
-h, --help Print help
quay info
Show metadata for a skill on the hub, without installing.
Usage
quay info [OPTIONS] <SKILL>
Fetches the hub's registry.json, locates the entry for <SKILL>, prints name, description, version, tags, source format, and the canonical path.
Examples
quay info hello
quay info hello --remote work
quay info hello --json | jq '.description'
Flags
| Flag | Effect |
|---|---|
--remote <NAME> | Use a specific remote. Default: profile default. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay search— discovery; useinfoonce you have a name.quay add— when you actually want it installed.
Caveats
- Returns an error if
<SKILL>is not in the registry. Possible causes: typo, hub'sregistry.jsonis stale (runquay rebuild-registry), or the skill was added without updating the registry (every modernquay pushupdates it; older flows didn't). - Does not download the skill body — just the registry entry. To read the full
SKILL.md, install it with--forceto a throwaway dir, or browse the hub on GitHub.
--help
Show metadata for a skill (without installing)
Usage: quay info [OPTIONS] <SKILL>
Arguments:
<SKILL>
Options:
--project <PROJECT> Project root (defaults to current directory)
--remote <REMOTE>
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
-h, --help Print help
quay init
Initialise quay in the current project.
Usage
quay init [OPTIONS]
Creates a .quay/ directory with an empty config.toml. The project config is layered on top of ~/.config/quay/config.toml — anything you set here overrides the user-level value for this project only.
Examples
quay init # current dir
quay init --project ~/code/foo # explicit
Confirm:
ls .quay/
# config.toml
Flags
| Flag | Effect |
|---|---|
--project <PATH> | Project root (default: cwd). |
--user-config <PATH> | Override user config path. |
--profile <NAME> | Override active profile for this invocation. |
--json | JSON output. |
When to use this vs …
- Use
quay initonce per repo if you want a per-project override (different mirror dirs, custom remote pinned to that project). - If your user-level config already covers everything,
initis optional —add/pushwork fine without it.
Caveats
- Re-running
initon a project that already has.quay/config.tomlleaves the existing file untouched. There is no--force. - Commit
.quay/config.tomlso teammates inherit the project config..quay/push-log.jsonis gitignored (local-only intent log).
--help
Initialize quay in the current project
Usage: quay init [OPTIONS]
Options:
--project <PROJECT> Project root (defaults to current directory)
--user-config <USER_CONFIG> Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE> Override the active profile for this invocation
--json Output JSON instead of human-readable text
-h, --help Print help
quay profile
Manage user profiles — bundles of identity (commit email) + remotes + install settings.
Usage
quay profile <SUBCOMMAND>
Subcommands:
| Subcommand | What |
|---|---|
list | Show all profiles. Active one is marked. |
current | Print the active profile name. |
add | Create a new profile (three modes — see below). |
use | Switch the active profile. -i for interactive picker. |
remove | Delete a profile (cannot remove the last one). |
show | Print full profile contents (TOML-shaped). |
rename | Rename a profile. |
edit | Change a profile in place: --email, --from-toml <path|->, or -i for the wizard pre-filled with current values. |
The three add modes
- Wizard —
quay profile add -i. Walks you through name, email, hub URL, push_mode. Recommended for first-time setup. - Explicit flags —
quay profile add work --email me@corp --remote main=git@github.com:corp/skills.git --provider github --push-mode pr --default --activate. Repeatable--remoteto seed several;--provider/--push-mode/--defaultapply to the most recent--remote. - From TOML —
quay profile add ci --from-toml /etc/quay/ci.toml(or-for stdin). Lets you commit profile fragments alongside CI configs.
Examples
quay profile add -i # wizard
quay profile add work --email me@corp \
--remote main=git@github.com:corp/skills.git \
--provider github --push-mode pr --default --activate
quay profile add ci --from-toml ./ci-profile.toml
quay profile use -i # picker
quay profile use personal
quay profile show work
quay profile rename work corp
quay profile remove old-profile
Flags
Common globals apply (--project, --user-config, --profile, --json). The add subcommand carries the most flags — see quay profile add --help.
When to use this vs …
quay remote add— adds a remote to the currently active profile.profile addcreates the profile + can seed remotes in one go.QUAY_PROFILEenv var — one-shot override at invocation time.profile usemakes the switch persistent.
Caveats
- The "last profile" cannot be removed. quay always has at least one profile.
--activateonly matters duringadd. Usequay profile use <name>afterwards to switch.- A profile without any remotes is valid but unhelpful — most commands will error with "no remotes configured." Add one with
quay remote add.
--help (top level)
Manage user profiles (multi-org identities + remote bundles)
Usage: quay profile [OPTIONS] <COMMAND>
Commands:
list List all profiles, marking the active one
current Print the active profile name
add Add a new profile
use Set the active profile
remove Remove a profile (cannot remove the last one)
show Print full profile contents
rename Rename a profile
edit Edit an existing profile
help Print this message or the help of the given subcommand(s)
quay remote
Manage the hub remotes in this project's .quay/config.toml. add, edit and
remove write that file, so run quay init first.
Usage
quay remote <SUBCOMMAND>
Subcommands:
| Subcommand | What |
|---|---|
add | Register a new remote (<NAME> <URL>). |
list | Show every remote with provider + push_mode + default marker. |
test | Run a connectivity probe (git archive or shallow clone). |
edit | Change a remote's URL, provider, push mode, direct branch, or default flag (<NAME>). |
remove | Delete a remote from the project config. |
Examples
quay remote add main git@github.com:corp/skills.git --provider github --default
quay remote add personal git@gitlab.com:me/skills.git --provider gitlab --push-mode pr
quay remote list
quay remote test main
quay remote remove personal
add flags
| Flag | Effect |
|---|---|
--default | Mark as the default remote. |
--provider <KIND> | Force provider: github / githubenterprise / gitlab / bitbucket / azuredevops. Auto-detected from URL if omitted. |
--push-mode <pr|direct> | Default push mode for this remote (default: pr). |
--direct-branch <BRANCH> | Target branch for direct-mode pushes (on edit, "" clears it). |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
quay profile add… --remote— seeds remotes during profile creation. Useremote addafterwards.quay rebuild-registry<name>— recovery flow when a hub'sregistry.jsonis broken.
remote test
Two-step probe:
git archive --remote=<URL> HEAD -- registry.json(cheap; no clone).- If
archivefails, shallow-clone (--depth 1) into a temp dir and look forregistry.json.
Exits 0 on success, non-zero on either failure with a diagnostic message identifying which step failed. Useful in CI before quay add to fail fast on bad URLs / missing creds.
Caveats
- A
gitlabURL that points at a GitLab Enterprise instance still works; the provider only affects PR creation, not git clone. - Removing the default remote leaves the project config with no default (a default from your profile still applies, if it has one). Otherwise the next push must use
--remote. testwill not catch every authentication issue (e.g. SSH known_hosts prompts) — if it succeeds butpushfails, suspect agent forwarding / known_hosts.
--help (top level)
Manage configured hub remotes
Usage: quay remote [OPTIONS] <COMMAND>
Commands:
add Add a new remote
test Test connectivity to a configured remote
list List configured remotes
remove Remove a remote
edit Edit an existing remote
help Print this message or the help of the given subcommand(s)
quay link
Manage mirror dirs declared under [install].mirrors in the active profile.
Usage
quay link [OPTIONS] [SUBCOMMAND]
Subcommands:
| Subcommand | What |
|---|---|
check | Verify mirrors are intact; exit non-zero if drifted. |
add <PATH> | Add a mirror to the project config and populate it for every installed skill. |
remove <PATH> | Remove a mirror from the project config (does not delete files). |
With no subcommand, quay link applies the configured mirrors for the project (idempotent).
Examples
quay link # apply mirrors
quay link check # CI: fail if mirrors drifted
quay link add .cursor/rules # add a Cursor mirror
quay link add .claude/skills --strategy auto
quay link remove .cursor/rules # forget the mirror (files stay)
Flags
| Flag | Effect |
|---|---|
--strategy <NAME> (on add) | Mirror strategy. Default auto (let quay pick per skill format). |
--force | Overwrite existing entries even if they conflict with quay's layout. |
--profile, --user-config, --project, --json | Standard globals. |
Mirror edits & reconcile
Mirrors (.claude/, .codex/, .cursor/) mirror the canonical .agents/skills/.
Symlink/junction mirrors are transparent, but a copy mirror (Windows, or
strategy = "copy") is a real directory — and hand-editing it used to be lost
silently on the next apply. quay link now detects and protects those edits:
quay link checkreports a copy mirror whose content differs from canonical (mirror content differs from canonical) and exits non-zero. It is read-only — it never creates or overwrites anything.quay linkrefuses to overwrite a diverged mirror. Copy your edit into the canonical skill and re-run, orquay link --forceto discard the mirror edit.- Discovery is disk-driven:
quay linkscans all known tool dirs (.agents/.claude/.codex/.cursor), so a mirror someone added outside the config is still seen. If no.agentscanonical exists, a lone tool dir is treated as the source of truth and left untouched.
Adopting an unmanaged dir (install.auto_link)
When quay link finds an unmanaged tool dir whose content is byte-identical to
canonical, it can adopt it — convert it to a managed mirror (a symlink on
most platforms) and register it as a mirror. This is opt-in: the first interactive run asks, and records your choice
as install.auto_link in .quay/config.toml (true = adopt, false = report
only). Non-interactive runs (--json, CI) never adopt or write config; they
report the dir and exit non-zero so the decision surfaces.
When to use this vs …
quay add— already applies your current mirrors.linkis for changing the mirror set or auditing it.quay scan— does not check mirror drift; it checks skill drift. Pairscan+link checkfor a full audit.
Caveats
link removedeletes the config entry, not the directory on disk. To also clean files,rm -rfafterwards.link checkis the canonical CI gate. Exit code 0 = clean; non-zero = at least one mirror is missing or out-of-sync.link checkis read-only — it detects drift but never creates or overwrites mirrors.- Without
--force,quay linkrefuses to overwrite a mirror whose content diverged from canonical (your edit is preserved).--forceoverwrites diverged mirror dirs without backup — use carefully.
--help
Apply or verify mirrors, reconciling canonical against known tool dirs on disk
Usage: quay link [OPTIONS] [COMMAND]
Commands:
check Verify mirrors are intact; exit non-zero if drifted
add Add a new mirror to the project config and apply it for every installed skill
remove Remove a mirror from the project config (does not delete the mirror dir)
help Print this message or the help of the given subcommand(s)
quay agents
Mirror your installed skills into any supported coding agent's skill directory, using quay's built-in agent registry — no per-agent config to hand-write.
The registry (~80 agents: Claude Code, Codex, Cursor, Gemini CLI, …) is
generated from vercel-labs/skills and
compiled into the binary. It tracks upstream automatically via CI; there is no
Node dependency at runtime.
Usage
quay agents list
quay agents link [--agent <id>]... [--global] [--force]
How it works
One canonical copy of each skill lives under .agents/skills/. Every targeted
agent gets a mirror (symlink, or copy where symlinks aren't available) pointing
back at it — a single source of truth.
flowchart LR
U[vercel-labs/skills<br/>src/agents.ts] -->|CI: scripts/sync-agents| T[data/agents.toml<br/>compiled into binary]
T --> R{quay agents}
R -->|list| L[show agents<br/>● = detected]
R -->|link --agent X| C[.agents/skills/<skill><br/>canonical]
C -->|symlink / copy| M1[.claude/skills/<skill>]
C -->|symlink / copy| M2[~/.cursor/skills/<skill>]
C -.->|universal agent:<br/>reads canonical directly| M3[.agents/skills — no mirror]
quay agents list
Lists every agent in the registry. A ● marks agents detected on this machine
(their config directory exists).
quay agents link
--agent <id>(-a) — target a specific agent (repeatable). Omit to target every detected agent.--global(-g) — install into the user-level directory (e.g.~/.claude/skills) instead of the project.--force— replace an existing entry that conflicts with quay's layout.
For project scope, the created mirrors are recorded under [install].mirrors
in .quay/config.toml (when the project is initialized), so quay link
and quay link check keep them in sync afterwards. Global mirrors are
machine-specific and are never persisted.
Agents whose directory is the canonical .agents/skills (Codex, Cursor,
Gemini CLI, …) read it directly, so no mirror is created for them.
Note:
eveuses apackage.json-based detection that can't be expressed as a path check, so it is never auto-detected — target it explicitly with--agent eve.
quay rebuild-registry
Regenerate a hub's registry.json from disk truth.
Usage
quay rebuild-registry [OPTIONS] [REMOTE]
Clones the remote, walks skills/<name>/SKILL.md, builds a fresh { skills: { <name>: { … } } } map, commits it, and pushes per the remote's push_mode. [REMOTE] defaults to the profile's default remote when omitted.
Examples
quay rebuild-registry # default remote
quay rebuild-registry work
quay rebuild-registry work --push-mode pr # force PR even if remote says direct
Flags
| Flag | Effect |
|---|---|
--push-mode <pr|direct> | Override the remote's push_mode. |
--profile, --user-config, --project, --json | Standard globals. |
When to use this vs …
- Recovery —
quay search/quay addreturn "skill not found" but the file exists on the hub. The registry has drifted; runrebuild-registry. - Migration — you imported skills into the hub manually (not via
quay push). Rebuild fixes the registry in one shot. - Bad seed — a hub started with a malformed
registry.json(e.g.{ skills: [] }array instead of map). Rebuild regenerates the correct shape. quay push— already updates the registry on every push. Reach forrebuild-registryonly when the registry is wrong.
Caveats
- Rewrites
registry.jsonfrom scratch. Any hand-edits there are lost. - The commit message is a stock "rebuild registry" message; if you want a custom one, edit before the PR is merged.
- Works against hubs with no
registry.jsonat all (creates one).
--help
Rebuild a hub's `registry.json` from disk truth and push it back.
Clones the remote, walks `skills/<name>/SKILL.md`, regenerates `registry.json` containing every discovered skill, then commits and pushes (PR or direct per the remote's `push_mode`).
Usage: quay rebuild-registry [OPTIONS] [REMOTE]
Arguments:
[REMOTE]
Remote name. Uses the default remote when omitted
Options:
--project <PROJECT>
Project root (defaults to current directory)
--push-mode <PUSH_MODE>
Override the remote's `push_mode` for this invocation
[possible values: pr, direct]
--user-config <USER_CONFIG>
Override user config path (defaults to ~/.config/quay/config.toml)
--profile <PROFILE>
Override the active profile for this invocation
--json
Output JSON instead of human-readable text
-h, --help
Print help (see a summary with '-h')
Share a skill with a team
You wrote a skill that works. You want everyone on the repo to get it with one command, and you want new joiners to get it without asking you which hub to point at. The trick is to put the hub in the project config, not in each person's profile, and commit it.
Point the project at the hub
quay init # creates .quay/config.toml + .agents/skills/
quay remote add team git@github.com:acme/skills-hub.git --default
quay remote test team # fail now, not on first push
quay remote add writes to .quay/config.toml in the project, not to
~/.config/quay/config.toml. That is what makes this work: commit the file and
every clone of the repo knows about team.
git add .quay/config.toml
git commit -m "chore: point quay at the team skills hub"
Publish the skill
quay validate csv-parse --strict # offline frontmatter check, no clone
quay push csv-parse --bump minor
With the default push_mode = pr, quay clones the hub, writes
skills/csv-parse/, updates registry.json, commits on a quay/<name>-<version>
branch and opens a PR through gh / glab / az. Without the provider CLI it
prints a compare URL instead. Merge the PR — until then nobody can install it.
Teammates install it
git pull # picks up .quay/config.toml
quay add csv-parse
The skill lands in .agents/skills/csv-parse/, and in any mirror directories the
project or profile configures. Whether you commit .agents/skills/ itself is
your call — quay works either way.
When you push a new version later, teammates run quay outdated to see it and
quay update csv-parse to take it.
Gotchas
- Remote names collide. A project remote overrides a profile remote with the
same name. If a teammate already has a personal remote called
teampointing somewhere else, the project's wins inside this repo — pick a name that says what it is. - Commits need an email.
quay pushrefuses to commit without one. It comes from the active profile (quay profile add … --email) or a[user]section in.quay/config.toml— don't put the latter in a shared file. - Auth is git's, not quay's. Everyone who installs needs read access to the hub over whatever URL you committed. An SSH URL locks out people who only have HTTPS credentials; see SSH key setup.
CI install via --from-toml
A CI job has no profile, no wizard, and no terminal. quay profile add --from-toml turns a TOML file you keep in the repo into a profile in one
non-interactive step, so the job can run quay add like a developer would.
The profile file
A top-level email plus [remotes.<name>] tables — not the on-disk
[profiles.<name>.user] shape. The profile name comes from the command line.
Unknown keys are ignored, so a pasted [user] table silently loses the email:
# ci/quay-profile.toml
email = "ci-bot@acme.dev"
[remotes.team]
url = "https://github.com/acme/skills-hub.git"
default = true
# provider = "github" # optional; auto-detected from the URL
# push_mode = "pr" # optional; only matters if CI pushes
- reads it from stdin instead, if you'd rather template it in the job.
The job
export QUAY_CFG="$RUNNER_TEMP/quay/config.toml"
quay --user-config "$QUAY_CFG" profile add ci --from-toml ci/quay-profile.toml --activate
quay --user-config "$QUAY_CFG" remote test team
for s in csv-parse release-notes; do
quay --user-config "$QUAY_CFG" add "$s"
done
quay add takes one skill name per call, hence the loop.
What you'll see
quay add prints installed <name> per skill; pass --json for a parseable
{"action": "installed", "skill": …} object instead. quay remote test prints a
✓ line with the size of the hub's registry.json, or a ✗ line naming the failure
kind (auth failed, unreachable, no registry) and exits 1.
Gotchas
profile addrefuses an existing name. On a runner that keeps its home directory between jobs, the second run fails. Pointing--user-configat a per-job temp path, as above, sidesteps it.- No TTY changes defaults, safely. Bare
quay addwith no skill name errors instead of opening the picker. A skill already on disk with different content is not overwritten — quay prints the verdict and diff, then exits non-zero telling you to re-run with--force. With--force, local files the new version lacks are kept unless you also pass--delete-extra. - Credentials. quay shells out to
git, so the job needs whatever git needs for that URL — a deploy key for SSH, or a token-backed credential helper for HTTPS. Nothing in the TOML carries a secret, and nothing should. QUAY_PROFILEselects a profile for every invocation if you'd rather not pass--activate;--profileon the command line still wins over it.
Multi-org profiles
You contribute skills to your employer's hub and to your own. The two need different commit emails, different remotes, and — the part that bites — you never want a work skill pushed to the personal hub by accident. A profile bundles an identity with its remotes; switch profiles and everything follows.
One profile per org
quay profile add work --email you@acme.dev \
--remote hub=git@github.com:acme/skills-hub.git --default
quay profile add personal --email you@hey.com \
--remote hub=git@gitlab.com:you/skills.git --provider gitlab --default \
--activate
Both profiles can call their remote hub — remotes are scoped to the profile.
--provider, --push-mode, --direct-branch and --default apply to the
--remote written just before them. quay profile add -i asks the same
questions one at a time if you'd rather not type URLs on a command line.
Pick the profile
Four ways, highest priority first:
| How | Scope |
|---|---|
--profile work | This one command. |
QUAY_PROFILE=work | This shell, or one CI job. |
profile = "work" in the project's .quay/config.toml | Everyone working in this repo. |
quay profile use work | Your machine, until you switch again. |
The project pin is the one that prevents the accident: commit it in your
employer's repos and quay push there resolves to work no matter what you last
ran profile use on.
# .quay/config.toml
profile = "work"
Check where you are
quay profile current # the name that resolves here
quay profile show work # that profile's email, remotes and push modes
quay profile list # all of them, active one marked
Run current from inside the repo — it accounts for the project pin and
QUAY_PROFILE, which profile list and a bare profile show do not. Pass
show the name current printed; with no name it shows active_profile.
Gotchas
quay remote add|edit|removecurrently drop the pin. They rewrite.quay/config.tomlwithout the top-levelprofile = "…"line — re-add it after changing remotes, and checkgit diffbefore committing.- A pinned profile must exist. If
.quay/config.tomlpinsworkand your user config has no profile by that name, quay errors instead of falling back. That's deliberate; create the profile. - More than one profile and nothing selected is an error, not a guess. With exactly one profile, it's used automatically.
- The commit author name isn't a flag.
--emailsets the email; the name comes fromnameunder[profiles.<name>.user]in~/.config/quay/config.toml, and is a placeholder until you set it there. quay remote adddoes not touch profiles. It writes the project config. To change a profile's remotes, usequay profile edit <name> -ior--from-toml.- Two keys for one host. If both orgs live on github.com under different SSH keys, see SSH key setup.
Direct vs PR push
quay push delivers a skill to the hub one of two ways. Which one is right
depends on who reviews skills on that hub, not on taste.
pr (default) | direct | |
|---|---|---|
| Lands on | A quay/<name>-<version> branch, plus a PR | The hub's default branch (or direct_branch) |
Visible to quay add | After someone merges | Immediately |
| Needs | gh, glab or az to open the PR — otherwise a compare URL is printed | Only git |
| Works on | GitHub, GitLab, Azure DevOps; Bitbucket via compare URL | Any git host |
| Protected branches | Fine | Rejected by the host |
Use PR for a shared hub where a second pair of eyes on a skill matters — a skill is instructions your colleagues' agents will follow. Use direct for a personal hub, or a team hub whose policy is "trust, then revert".
Set it per remote
quay remote add mine git@gitlab.com:you/skills.git --push-mode direct
quay remote edit team --push-mode pr
Or in a profile: quay profile add … --remote hub=<url> --push-mode direct.
The value is stored as push_mode on the remote.
Override it once
quay push csv-parse --push-mode direct
quay push csv-parse --push-mode pr # remote says direct, this one wants review
Direct to a branch other than the default
Teams that integrate on develop can send direct pushes there:
quay remote edit team --push-mode direct --direct-branch develop
quay push csv-parse --direct-branch staging # just this once
There is no push-time way back to the default branch: --direct-branch "" is
the same as leaving the flag off, so the remote's direct_branch still wins.
Clear it on the remote instead — quay remote edit team --direct-branch "".
If the branch doesn't exist on the hub yet, quay creates it from the default
branch. --direct-branch is ignored when the push mode resolves to pr.
What you'll see
PR mode prints the branch and the PR URL — or, when no provider CLI is
available, a URL to open the PR by hand. Direct mode prints the branch and a
short commit SHA. --json reports either as one object with a mode field. Both are recorded in
~/.config/quay/push-log.json, which is how quay scan knows a skill was pushed.
Gotchas
direct_branch+quay add. Installs read whatever branch is configured as the remote'sdirect_branch. A skill pushed directly todevelopis installable fromdevelop— not from the default branch until someone merges.remote editedits the project config only. A remote that lives in a profile is changed withquay profile edit.rebuild-registryhonors the samepush_mode(and takes--push-mode).remove --remote/--everywheredoes not: it always pushes directly.
Force update over local edits
You tweaked an installed skill, the hub has since moved on, and you want the
hub's copy back — or you aren't sure what changed and want to look before you
lose anything. quay never resolves this silently: a plain quay add on a skill
you already have shows you the difference and stops.
Look first
quay scan # status column: installed-modified = edited since install
quay outdated # version bumps, and content that differs whatever the version says
quay diff csv-parse
quay diff compares the whole skill directory and gives a verdict from hub
history — hub ahead, your copy ahead, or changed in an unknown direction — then
a per-file diff. + lines are what the hub would give you, - lines are what
you'd lose. It writes nothing.
If any of your edits are worth keeping, push them first
(quay push csv-parse) or copy them somewhere. Nothing below has an undo
except your project's own git history.
Take the hub's copy
quay add csv-parse --force
This overwrites every file the hub's version contains. It skips the reconcile step entirely — no verdict, no prompt.
Without --force, in a terminal, quay add csv-parse prints the verdict and
diff and asks what to do with that one skill. Use that when you want to decide
per skill rather than overwrite.
Files only you have
The hub's version may not contain every file in your copy — notes you added, or a file the hub deleted since. quay can't tell those apart, so in a terminal it lists them and asks. Decide up front instead:
quay add csv-parse --force --keep-extra # keep them, no prompt
quay add csv-parse --force --delete-extra # exact hub copy, no prompt
With no terminal and neither flag, they are kept. Dotfiles and symlinks are never touched.
Several at once
quay add -i --force
In the picker, --force overwrites every selected skill. Without it, quay asks
once for all collisions: update all, skip all, or prompt per skill.
Gotchas
quay updatewon't help with drift — for frontmatter skills. It acts on version upgrades there. A skill whose content differs at the same version — your edit, or a hub fix that didn't bumpversion— needsquay add --force. Slash-command and freestyle skills (no YAML frontmatter) are compared by content, so for themupdatedoes act on drift — and overwrites your edits. A frontmatter skill that merely omitsversionis not: it counts as0.0.0on both sides, soupdateskips it — usequay add --force.- Mirrors follow. Configured mirrors (
[install].mirrors) are re-applied after the install, so.claude/skills/csv-parseand friends get the new copy too. --delete-extratrusts the hub'sregistry.json. If the registry is stale, a file that still exists upstream can be deleted.quay rebuild-registryon the hub fixes the registry.
Seed an empty hub
A hub is any git repository with skills under skills/<name>/ and a
registry.json at the root. You don't build either by hand: the first
quay push creates both.
Create the repository
Create it on your git host with an initial commit — tick "Add a README" or equivalent. quay clones the hub and works from its default branch, so that branch has to exist. A repository with no commits at all has none.
The README is a good place to tell people what the hub is for and how to install from it:
quay remote add team <this repo's URL> --default
quay add <skill>
Register it and check access
quay remote add team git@github.com:acme/skills-hub.git --default
quay remote test team
remote test cannot pass yet — it looks for registry.json, and nothing has
been pushed. Read the reason it gives: no registry is expected here; auth
failed is not, and is worth fixing before the first push rather than after.
First push
quay validate csv-parse --strict
quay push csv-parse
quay writes skills/csv-parse/, generates registry.json with one entry, and
commits both — through a PR, or directly to the remote's direct_branch (the
default branch when that is unset), per the remote's push_mode. In PR mode, merge it; the hub is empty to quay add until you do.
quay remote test team # now reports registry.json and its size
quay search csv
Bringing a pile of existing skills
Two routes:
- One by one with
quay push. Each gets a registry entry with the right file list and content hash.quay push -iopens a checkbox picker over every local skill, so this is less tedious than it sounds. - Copy them in with git, then
quay rebuild-registry team. Commitskills/<name>/SKILL.mddirectories however you like, then let quay walk the tree and regenerateregistry.jsonfrom what's actually there.
Gotchas
- Don't hand-write
registry.json. A malformed one is replaced with a fresh one on the next push.rebuild-registryrewrites it from disk, discarding any hand edits. - Direct mode against a protected default branch fails on the very first push too — see Protected default branches.
- Bitbucket has no provider CLI; PR mode prints a URL for you to open the PR in the browser.
Protected default branches
Your hub's main requires reviews or status checks, and someone set the remote
to push_mode = "direct". The push gets as far as git push and the host says
no. quay doesn't silently retry another way — a direct push and a PR have
different review semantics, and switching between them is your call.
What you'll see
The hub's rejection, wrapped with a hint:
error: config validation: direct push to 'main' failed: git command failed at git push origin main: <git's error>; if the branch is protected, set this remote's push_mode = pr
Nothing reached the hub. The local skill is untouched.
Fix it for this push
quay push csv-parse --push-mode pr
Fix it for good
quay remote edit team --push-mode pr
For a remote defined in a profile rather than the project, use
quay profile edit <name> -i. pr is the default when push_mode is absent,
so deleting the line from the TOML works too.
Keep direct, change the target
If your team's rule is "anything may land on develop, main is protected",
keep direct mode and aim it at the unprotected branch:
quay remote edit team --direct-branch develop
quay push csv-parse # → develop
quay creates develop from the default branch if it doesn't exist yet. Note
that quay add and quay outdated then read the hub from develop too, so
consumers see the skill as soon as it's pushed — not when develop merges to
main.
Other commands that push
quay rebuild-registry writes to the hub in the remote's push_mode and fails
similarly against a protected branch; its hint says to pass --push-mode pr,
which works as a one-off.
quay remove --remote / --everywhere always pushes directly, whatever
push_mode says, so against a protected branch it fails with no PR fallback.
Remove the skill by PR by hand there.
Gotchas
- Required signed commits. quay commits with your
gitand your git config, setting only the author name and email, so signing follows yourcommit.gpgsignsetting. If the hub requires signatures, turn signing on in git — quay has no flag for it. quay remote testwon't warn you. It only reads; branch protection only shows up on write.
SSH key setup
quay has no credentials of its own. Every clone, fetch and push is your git
binary, with your environment, talking to the URL in the remote's config. If
git clone <url> works in your shell, quay can read the hub; pushing (quay push)
also needs write access to the repository, which a clone does not prove. This page is the short path
to making that true over SSH.
One key, one host
ssh-keygen -t ed25519 -C "you@acme.dev"
ssh-add ~/.ssh/id_ed25519
Add ~/.ssh/id_ed25519.pub to your account on the host (GitHub: Settings → SSH
and GPG keys; GitLab: Preferences → SSH Keys; Azure DevOps: User settings →
SSH public keys). Then check git's side, then quay's:
ssh -T git@github.com
quay remote test team
remote test exits 0 with a ✓ line when it can read the hub's registry.json,
and 1 with a ✗ line saying auth failed, unreachable or no registry
otherwise.
Use the SSH form of the URL — git@github.com:acme/skills-hub.git, not
https://…. The provider is detected from the hostname in either form.
Two accounts on one host
Work and personal accounts on github.com need two keys, and SSH picks a key by host. Give each one an alias:
# ~/.ssh/config
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
Then use the alias in the remote URL, and name the provider — auto-detection
matches on hostnames like github.com or gitlab., and github-work isn't
one of them:
quay profile add work --email you@acme.dev \
--remote hub=git@github-work:acme/skills-hub.git --provider github --default
An unrecognized host is treated as GitHub, so a GitLab alias without
--provider gitlab will try to open its merge requests with gh.
Gotchas
- PRs don't use SSH. In PR mode, quay pushes the branch over SSH, then calls
gh,glaborazto open the PR — those authenticate separately (gh auth login,glab auth login,az login). Withgh, a missing login falls back to a printed compare URL. Withglaboraz, the command fails after the branch is already pushed — open the MR/PR by hand, or log in and re-run. - First-contact host keys. quay captures git's output, so an unknown-host
prompt from SSH is easy to miss and can stall a command. Run
ssh -T git@<host>once by hand to record the key inknown_hosts. - No agent in the session. Keys loaded in your desktop session aren't
necessarily visible to a CI job, a container or an IDE terminal. If
ssh-add -llists nothing there and noIdentityFile(or default key file under~/.ssh/) is readable, neither git nor quay has a key to offer. - SSH blocked entirely? Use the HTTPS URL with a credential helper. For
GitHub,
gh auth setup-gitwiresgh's token into git, and quay inherits it.
Vet a skill before quay add
quay add fetches Markdown that your agent will execute with implicit trust.
quay verifies provenance and integrity — that the file you got is the file the
remote has. It does not read the skill and judge whether the instructions inside
it are hostile. Nothing in quay does.
SkillSpector (NVIDIA, Apache-2.0) does that part: 68 patterns across 17 categories — prompt injection, data exfiltration, privilege escalation, supply chain, excessive agency, MCP tool poisoning, plus AST-level dangerous-code detection and taint tracking.
The two tools are not integrated and do not need to be. Run the scan, then add.
Scan before you add
# Scan the source repo without downloading it yourself
skillspector scan https://github.com/someone/their-skill --no-llm
# Happy with it?
quay add their-skill
Exit codes: 0 = risk score ≤ 50, 1 = over threshold, 2 = scan error. The
threshold is hard-coded in SkillSpector; there is no flag to move it.
--no-llm runs static analysis only. Without it, skill content is sent to an
LLM provider for semantic evaluation — worth it for a manual review of something
you distrust, wrong for anything automatic.
Audit what you already installed
# Everything in canonical, one summary table
skillspector scan .agents/skills/ --recursive --no-llm
Findings you have triaged and accepted go in a baseline so re-scans surface only what is new:
skillspector baseline .agents/skills/ -o .skillspector-baseline.yaml
skillspector scan .agents/skills/ --recursive --no-llm --baseline .skillspector-baseline.yaml
Commit the baseline.
Gate it in CI
If you want this on every PR, four things are worth knowing before you write the workflow — each one cost us a red run:
--recursivewith--format sarifwrites concatenated text, not merged SARIF. Scan each skill directory separately.- Uploading a directory of per-skill SARIF files is rejected: code scanning
refuses multiple runs sharing a category. Fold them into a single run (dedupe
tool.driver.rulesbyid) and upload one file. - Findings carry uris relative to the scanned dir (a bare
SKILL.md), so rewrite them to repo-root-relative or every annotation lands on a path that doesn't exist. - Exit code 1 means "over threshold"; anything else means the scan didn't happen. Treat them differently — an unscanned skill is not a safe skill, and collapsing both into one branch is how a crashed scanner produces a green check.
Pin the install to a commit. SkillSpector publishes no tagged releases and moves
fast; an unpinned git+https:// install means your CI gate changes without you.
Let the agent do it
Both tools ship MCP servers — quay mcp and skillspector mcp. An agent with
both configured can scan and then install in one flow, gating the install on the
result, with no wiring between the projects:
{
"mcpServers": {
"quay": { "command": "quay", "args": ["mcp"] },
"skillspector": { "command": "skillspector", "args": ["mcp"] }
}
}
Requires the MCP extra: uv tool install 'skillspector[mcp] @ git+https://github.com/NVIDIA/skillspector.git'.
Why this isn't built into quay
A pre-install gate inside quay add would mean a Python runtime in a Rust
tool's install path, and a hard dependency on a CLI contract that has no
released version yet. Scanning stays a separate step you compose, by choice.