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.json committed in plain text. git log is 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 diff works. 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

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 and git 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 at add -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 with interrupted, instead of a made-up io error at prompt. update -i also 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-extra had 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 check reports 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 single quay 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 update still reports what it changed. The skills updated before the failure — and their deleted_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 --json reports deleted_extras per skill — what was actually removed, as reported by the install itself.
  • quay diff --json reports commits_ahead and last_commit_date for hub_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_extras return the paths they deleted.

Docs

  • Eight recipe pages written; corrected add, remote, profile, update and the GitHub tutorial where they disagreed with the CLI.

0.15.3 — 2026-08-03

Performance

  • quay diff no 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 diff can 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 add no 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-level quay diff has 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 Option that was only ever None for unchanged files. No output changed — verified across six hub fixtures through both quay diff and quay 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 diff and quay 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 as no 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 diff against a real hub. Nothing was written to disk incorrectly; the verdicts were wrong, not the files.

0.15.0 — 2026-07-31

Changed

  • update and add --force now 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, so quay diff reported 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-extra and --delete-extra decide it outright and work unattended; --delete-extra names 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 --bump no 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: MIT came back as license: null, and compatibility, metadata and allowed-tools were dropped outright. allowed-tools is the only machine-readable statement of what a skill may do, and the hub copy is what everyone else installs. The bump now rewrites the version: 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 outdated says 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 --remote and --json.

    It compares the whole skill directory. The reconcile engine that backs quay add's collision prompt only ever looked at SKILL.md, so a skill whose references/ or scripts/ moved on the hub reported as identical. Diffs are pull-oriented: + is what the hub would give you.

Fixed

  • quay outdated no 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 edited references/ without touching version left 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 existing local -> <version> upgrade rows. --json and the quay_outdated MCP tool gain a content_drift field so the two reasons can be told apart.

    Drift is direction-neutral: two hashes prove the bytes differ, not which side changed them. quay update still acts on version upgrades only, so drift rows point at quay add <name> --force.

    Line endings are normalized before comparing, so a Windows checkout (core.autocrlf) does not report every installed skill as drifted. The published content_hash is unchanged — no registry needs regenerating.

0.13.4 — 2026-07-23

Security

  • registry.json file paths are validated before anything is fetched. The registry is downloaded from the remote hub, and its files list 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.autocrlf rewrites line endings on checkout, so on Windows every frontmatter skill silently degraded to "freestyle" — losing its name, description and version, and listing as unversioned.
  • Windows: --force can replace a symlinked mirror. Unlinking used remove_file, which fails on the directory symlinks and junctions that mirrors are on Windows, so quay link --force, quay add --force and quay agents link --force could not replace an existing mirror.
  • Dot-prefixed directories are never treated as skills. A staging directory left behind by an interrupted quay add could appear in quay list as a skill named .tmpAbCdEf, and quay link would mirror it into every tool directory.

Internal

  • CI runs cargo fmt, cargo clippy and 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.toml at 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 add no 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 with AlreadyExists. The fetch now stages in a temporary directory and is renamed into place only once every file has landed.
  • A failed --force install no longer destroys the existing skill. The previous copy is moved aside and restored if the replace fails, rather than deleted up front.
  • quay update preserves 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 link reported 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 link scans 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 link refuses to overwrite a mirror whose content diverged from canonical. Copy-strategy mirrors were previously re-materialised unconditionally and hand edits were lost silently. Pass --force to discard the mirror edit, or copy it into the canonical skill first.
  • quay link check is 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 — write SKILL.md directly, with your editor, an AI agent, or any tool.
  • skills.lock.json is no longer written. If a legacy lockfile exists, quay prints a one-time notice and instructions to delete it.

Changed

  • quay add JSON output no longer returns version/remote fields — 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.

ToolDoesAnnotation
quay_searchsearch hubs for skillsread-only, network
quay_infometadata for one skillread-only, network
quay_listskills installed here (canonical)read-only
quay_outdatedinstalled skills with newer hub versionsread-only, network
quay_scanevery SKILL.md on disk (all mirrors)read-only
quay_validatecheck a skill's frontmatterread-only
quay_addinstall a skillwrite, network
quay_linkmirror into .claude/, .codex/, …write
quay_updatepull newer versionwrite, network
quay_removeuninstall a skillwrite, destructive
quay_pushpublish a skill (opens a PR / direct push)write, network, outward
quay_remoteadd a hub remote to project configwrite, 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-core directly (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/push need remotes configured there — see remote and config.toml.
  • Transport is stdio. No ports, no auth — it inherits your existing git credentials 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:

ClientFileWrapper keyNotes
Claude Code——claude mcp add -s user quay -- quay mcp (CLI, user scope)
Codex~/.codex/config.toml[mcp_servers.quay]TOML
Cursor.cursor/mcp.jsonmcpServers
VS Code.vscode/mcp.jsonserversrequires "type": "stdio"; also code --add-mcp
opencodeopencode.jsonmcp"type": "local", command is an array
Devin.devin/config.jsonmcpServerscloud Devin needs quay installed in its VM
genericanyvariesuniversal 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 mcp is 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_push is 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:

  1. quay clones your default-profile hub into a temp dir.
  2. Copies SKILL.md to skills/early-returns/SKILL.md.
  3. Updates the hub's registry.json with name, description, version, tags.
  4. Commits.
  5. Per your remote's push_mode: opens a PR (default) or pushes the branch directly.
  6. Prints the PR URL or commit SHA.
  7. 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

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

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:

  1. Generate a key: ssh-keygen -t ed25519 -C "you@example.com".
  2. Add the public key at https://github.com/settings/keys.
  3. 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

FormUse
git@github.com:org/repo.gitSSH. Auto-detected as github.
https://github.com/org/repo.gitHTTPS. Auto-detected as github.
git@ghe.example.com:org/repo.gitGitHub 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 main is protected (required reviews, status checks), push_mode = "direct" will fail at git push origin main. Use push_mode = "pr" (the default) or pass --push-mode pr.
  • The PR is opened against the default branch, not necessarily main. quay reads the remote's HEAD ref 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

SymptomFix
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 pushCDN cache; wait ~60s or quay rebuild-registry.

Next

GitLab

GitLab-specific setup. Works for gitlab.com and self-hosted GitLab CE/EE.

Auth

SSH is the typical path:

  1. ssh-keygen -t ed25519 -C "you@example.com".
  2. Paste the public key at https://gitlab.com/-/user_settings/ssh_keys.
  3. 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

FormUse
git@gitlab.com:group/repo.gitSSH. Auto-detected as gitlab.
https://gitlab.com/group/repo.gitHTTPS. Auto-detected as gitlab.
git@git.corp.example:platform/skills.gitSelf-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 on master. quay reads HEAD after clone, so either works without configuration.
  • Default branch settings 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 with remote: 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

SymptomFix
glab: command not foundInstall 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 glabToken expired or scope missing. glab auth login again.
MR opens but immediately closesProject 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:

FormUse
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

SymptomFix
az: command not foundInstall Azure CLI + azure-devops extension.
TF401019 (no access)PAT scope wrong; regenerate with Code (Read & write).
RemoteRejection: pre-receive hookBranch policy. Use --push-mode pr.
pull request creation failed from azaz 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:

  1. ssh-keygen -t ed25519 -C "you@example.com".
  2. Paste the public key at https://bitbucket.org/account/settings/ssh-keys/.
  3. Test: ssh -T git@bitbucket.org.

HTTPS uses an App Password, not your account password:

  1. https://bitbucket.org/account/settings/app-passwords/ → Create app password.
  2. Grant Repositories: Read, Write.
  3. Use it as the password when prompted; cache via credential helper.

URL formats

FormUse
git@bitbucket.org:workspace/repo.gitSSH. Auto-detected as bitbucket.
https://bitbucket.org/workspace/repo.gitHTTPS. 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

SymptomFix
Permission denied (publickey)SSH key not added at account-settings → SSH keys.
401 over HTTPSUsing account password instead of App Password. Generate one.
pre-receive hook declined on direct pushBranch restriction. Use --push-mode pr.

Next

CLI overview

quay is a single binary with fifteen subcommands. Every subcommand respects four global options:

FlagEffect
--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.
--jsonEmit machine-readable JSON on stdout instead of a human table.

Subcommand groups

Project setup

  • init — drop a .quay/config.toml skeleton 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 hub registry.json from 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:

CodeMeaning
0Success
1Generic command failure
2clap 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

FlagEffect
--remote <NAME>Use a specific remote instead of the profile default.
--forceOverwrite the skill even if it already exists locally.
-i, --interactiveOpen the checkbox picker. Mutually exclusive with SKILL.
--keep-extraWith --force, keep local files the new version does not contain, without prompting or printing the note.
--delete-extraWith --force, delete local files the new version does not contain, without prompting.
--profile, --user-config, --project, --jsonStandard globals.

When to use this vs …

  • quay update — refresh an already installed skill to the latest hub version. add errors with "already installed" unless --force.
  • quay info — preview metadata without writing files.

Caveats

  • A skill that fails quay validate on 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.
  • --force overwrites every file the fetched version contains, without confirmation — local edits to SKILL.md are lost. Files it doesn't contain are a separate question; see below. Use quay outdated first 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

FlagEffect
--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, --interactiveOpen the checkbox picker.
--profile, --user-config, --project, --jsonStandard globals.

When to use this vs …

  • quay rebuild-registry — regenerate a corrupted/missing registry.json. Use after manual hub surgery, not as a normal publish step.
  • quay validate first — catches frontmatter mistakes offline before incurring a hub clone.

Caveats

  • --bump requires Frontmatter format. SlashCommand / Freestyle skills get a clean error if you pass --bump against them.
  • Direct mode against a branch-protected default branch will fail at git push origin main. The error hint suggests --push-mode pr or setting push_mode = "pr" in the remote config.
  • The first push to an empty hub seeds registry.json automatically — 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

FlagEffect
--everywhereAlso commit a deletion to every remote publishing this skill (respects per-remote push_mode).
-i, --interactivePick from installed skills.
--profile, --user-config, --project, --jsonStandard globals.

When to use this vs …

  • quay update — to refresh, not uninstall.
  • quay link remove <path> — remove a mirror directory from the project config, not a skill.

Caveats

  • --everywhere deletes from the remote without a confirmation prompt. Combined with push_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), remove still 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

FlagEffect
--dry-runShow what would change without writing.
-i, --interactiveOpen the picker.
--allSkip the TTY auto-picker; update everything outdated.
--keep-extraKeep local files the new version does not contain, without prompting or printing the note.
--delete-extraDelete local files the new version does not contain, without prompting.
--profile, --user-config, --project, --jsonStandard 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 version field. Skills without frontmatter (SlashCommand / Freestyle) never show as outdated by version — they're compared by SHA-on-fly instead.
  • update overwrites the installed copy with the hub's. Edits you made to files the hub also ships are replaced; check quay 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 --json too); quay link --force overwrites 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 single quay link --force.
  • If a skill fails partway through a multi-skill run, the skills already updated are still reported (and in --json, with their deleted_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-run does 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.
FlagEffect
--keep-extraKeep them, no prompt, and no note either — this is the only way to suppress it.
--delete-extraDelete 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

FlagEffect
--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, --jsonStandard 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.json per remote. If the hub was just pushed to seconds ago, you may get a stale result — ?_cb cache-busting in the fetcher mitigates this, but five-minute CDN tail can still bite. Retry after a minute.
  • A hub with a malformed registry.json is 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:

  1. Version upgrade — the hub's frontmatter version is semver-greater than the installed copy's. Reported as local -> <version>.
  2. Content drift — the local bytes differ from the hub's published content_hash, whatever the versions say. Reported as differs 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

FlagEffect
--profile, --user-config, --project, --jsonStandard globals.

When to use this vs …

  • quay update --dry-run — overlapping but narrower: update acts on version upgrades only, so it will not offer to resolve a content-drift row. Use outdated when 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.json predates content-hash indexing publishes no content_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.autocrlf rewrites 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.json produces 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

VerdictMeaning
up to dateYour 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 hubThe hub's HEAD is an ancestor of the commit your copy matches.
differs …, direction is unknownNo 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 hubThe 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:

MarkMeaning
MPresent 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

FlagEffect
--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, --jsonStandard globals.

When to use this vs …

  • quay outdated — scans every installed skill and says which ones differ. Use it first; use diff to 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 hub needs the matching commit to sit off HEAD's ancestry, which only happens after the hub force-pushes or rewinds. A plain local edit reports direction is unknown instead.
  • 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's SKILL.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:

StatusMeaning
localAuthored here; never pushed.
installedCame from a hub via quay add.
installed-modifiedCame from a hub, but local file has been edited since install.
pushed-localAuthored 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

FlagEffect
--root <PATH>Override the canonical skills root (default: <project>/.agents/skills).
--jsonJSON output (also accepted as --json global).
--profile, --user-config, --projectStandard globals.

When to use this vs …

  • quay outdated — only reports drift against the hub. scan reports 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, format Freestyle). scan never errors on weird files; it just classifies.
  • --root affects discovery but does not rewrite the project config; if you regularly use a non-default root, set it in .quay/config.toml instead.

--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.
  • name matches the directory.
  • version is semver-valid (if present).
  • tags is a string array (if present).

No network calls.

Examples

quay validate hello
quay validate hello --strict
quay validate hello --json

Flags

FlagEffect
--strictTreat missing/required-field issues as errors (exit 1). Default: warns to stderr, exits 0.
--profile, --user-config, --project, --jsonStandard 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. --strict is what you want in CI.
  • SlashCommand and Freestyle skills produce a warning (no frontmatter) by design; --strict upgrades 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

FlagEffect
--remote <NAME>Use a specific remote. Default: profile default.
--profile, --user-config, --project, --jsonStandard globals.

When to use this vs …

  • quay search — discovery; use info once 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's registry.json is stale (run quay rebuild-registry), or the skill was added without updating the registry (every modern quay push updates it; older flows didn't).
  • Does not download the skill body — just the registry entry. To read the full SKILL.md, install it with --force to 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

FlagEffect
--project <PATH>Project root (default: cwd).
--user-config <PATH>Override user config path.
--profile <NAME>Override active profile for this invocation.
--jsonJSON output.

When to use this vs …

  • Use quay init once 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, init is optional — add / push work fine without it.

Caveats

  • Re-running init on a project that already has .quay/config.toml leaves the existing file untouched. There is no --force.
  • Commit .quay/config.toml so teammates inherit the project config. .quay/push-log.json is 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:

SubcommandWhat
listShow all profiles. Active one is marked.
currentPrint the active profile name.
addCreate a new profile (three modes — see below).
useSwitch the active profile. -i for interactive picker.
removeDelete a profile (cannot remove the last one).
showPrint full profile contents (TOML-shaped).
renameRename a profile.
editChange a profile in place: --email, --from-toml <path|->, or -i for the wizard pre-filled with current values.

The three add modes

  1. Wizard — quay profile add -i. Walks you through name, email, hub URL, push_mode. Recommended for first-time setup.
  2. 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 --remote to seed several; --provider / --push-mode / --default apply to the most recent --remote.
  3. 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 add creates the profile + can seed remotes in one go.
  • QUAY_PROFILE env var — one-shot override at invocation time. profile use makes the switch persistent.

Caveats

  • The "last profile" cannot be removed. quay always has at least one profile.
  • --activate only matters during add. Use quay 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:

SubcommandWhat
addRegister a new remote (<NAME> <URL>).
listShow every remote with provider + push_mode + default marker.
testRun a connectivity probe (git archive or shallow clone).
editChange a remote's URL, provider, push mode, direct branch, or default flag (<NAME>).
removeDelete 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

FlagEffect
--defaultMark 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, --jsonStandard globals.

When to use this vs …

  • quay profile add … --remote — seeds remotes during profile creation. Use remote add afterwards.
  • quay rebuild-registry <name> — recovery flow when a hub's registry.json is broken.

remote test

Two-step probe:

  1. git archive --remote=<URL> HEAD -- registry.json (cheap; no clone).
  2. If archive fails, shallow-clone (--depth 1) into a temp dir and look for registry.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 gitlab URL 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.
  • test will not catch every authentication issue (e.g. SSH known_hosts prompts) — if it succeeds but push fails, 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:

SubcommandWhat
checkVerify 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

FlagEffect
--strategy <NAME> (on add)Mirror strategy. Default auto (let quay pick per skill format).
--forceOverwrite existing entries even if they conflict with quay's layout.
--profile, --user-config, --project, --jsonStandard 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 check reports 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 link refuses to overwrite a diverged mirror. Copy your edit into the canonical skill and re-run, or quay link --force to discard the mirror edit.
  • Discovery is disk-driven: quay link scans all known tool dirs (.agents/.claude/.codex/.cursor), so a mirror someone added outside the config is still seen. If no .agents canonical exists, a lone tool dir is treated as the source of truth and left untouched.

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. link is for changing the mirror set or auditing it.
  • quay scan — does not check mirror drift; it checks skill drift. Pair scan + link check for a full audit.

Caveats

  • link remove deletes the config entry, not the directory on disk. To also clean files, rm -rf afterwards.
  • link check is the canonical CI gate. Exit code 0 = clean; non-zero = at least one mirror is missing or out-of-sync.
  • link check is read-only — it detects drift but never creates or overwrites mirrors.
  • Without --force, quay link refuses to overwrite a mirror whose content diverged from canonical (your edit is preserved). --force overwrites 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/&lt;skill&gt;<br/>canonical]
    C -->|symlink / copy| M1[.claude/skills/&lt;skill&gt;]
    C -->|symlink / copy| M2[~/.cursor/skills/&lt;skill&gt;]
    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).

  • --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: eve uses a package.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

FlagEffect
--push-mode <pr|direct>Override the remote's push_mode.
--profile, --user-config, --project, --jsonStandard globals.

When to use this vs …

  • Recovery — quay search / quay add return "skill not found" but the file exists on the hub. The registry has drifted; run rebuild-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 for rebuild-registry only when the registry is wrong.

Caveats

  • Rewrites registry.json from 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.json at 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 team pointing somewhere else, the project's wins inside this repo — pick a name that says what it is.
  • Commits need an email. quay push refuses 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 add refuses an existing name. On a runner that keeps its home directory between jobs, the second run fails. Pointing --user-config at a per-job temp path, as above, sidesteps it.
  • No TTY changes defaults, safely. Bare quay add with 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_PROFILE selects a profile for every invocation if you'd rather not pass --activate; --profile on 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:

HowScope
--profile workThis one command.
QUAY_PROFILE=workThis shell, or one CI job.
profile = "work" in the project's .quay/config.tomlEveryone working in this repo.
quay profile use workYour 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|remove currently drop the pin. They rewrite .quay/config.toml without the top-level profile = "…" line — re-add it after changing remotes, and check git diff before committing.
  • A pinned profile must exist. If .quay/config.toml pins work and 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. --email sets the email; the name comes from name under [profiles.<name>.user] in ~/.config/quay/config.toml, and is a placeholder until you set it there.
  • quay remote add does not touch profiles. It writes the project config. To change a profile's remotes, use quay profile edit <name> -i or --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 onA quay/<name>-<version> branch, plus a PRThe hub's default branch (or direct_branch)
Visible to quay addAfter someone mergesImmediately
Needsgh, glab or az to open the PR — otherwise a compare URL is printedOnly git
Works onGitHub, GitLab, Azure DevOps; Bitbucket via compare URLAny git host
Protected branchesFineRejected 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's direct_branch. A skill pushed directly to develop is installable from develop — not from the default branch until someone merges.
  • remote edit edits the project config only. A remote that lives in a profile is changed with quay profile edit.
  • rebuild-registry honors the same push_mode (and takes --push-mode). remove --remote / --everywhere does 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 update won'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 bump version — needs quay add --force. Slash-command and freestyle skills (no YAML frontmatter) are compared by content, so for them update does act on drift — and overwrites your edits. A frontmatter skill that merely omits version is not: it counts as 0.0.0 on both sides, so update skips it — use quay add --force.
  • Mirrors follow. Configured mirrors ([install].mirrors) are re-applied after the install, so .claude/skills/csv-parse and friends get the new copy too.
  • --delete-extra trusts the hub's registry.json. If the registry is stale, a file that still exists upstream can be deleted. quay rebuild-registry on 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 -i opens 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. Commit skills/<name>/SKILL.md directories however you like, then let quay walk the tree and regenerate registry.json from 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-registry rewrites 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 git and your git config, setting only the author name and email, so signing follows your commit.gpgsign setting. If the hub requires signatures, turn signing on in git — quay has no flag for it.
  • quay remote test won'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, glab or az to open the PR — those authenticate separately (gh auth login, glab auth login, az login). With gh, a missing login falls back to a printed compare URL. With glab or az, 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 in known_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 -l lists nothing there and no IdentityFile (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-git wires gh'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:

  • --recursive with --format sarif writes 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.rules by id) 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.

config.toml

SKILL.md

registry.json

push-log.json

Environment variables

Exit codes