start

Install #

Install with Homebrew on macOS, with Go, or download a binary for macOS, Linux, or Windows from Releases.

$ brew install --cask madalinpopa/tap/skills
$ go install github.com/madalinpopa/skills@latest

The CLI shells out to Git. It needs Git 2.45 or newer on your PATH and checks the version before every command that touches the store. An older Git is an error that names the version found.

No setup step. The first command that needs the store writes a default config and clones it. skills init does the same thing explicitly. Only the first run needs network access.

Quick start #

Run these from inside a project. Use a name from skills ls instead of demo.

$ skills init                 # create the config and clone the store
$ skills ls                   # list published skills
$ skills install demo         # install a skill into this project
$ skills ls --local           # list this project's installed skills
$ skills sync                 # fetch the latest store content
$ skills update               # update installed skills from the store
$ skills remove demo          # uninstall a skill

Add --dry-run to any writing command to see the plan without touching disk, and -v to list each file. Run skills <command> --help for all options.

How it works #

github repo          ~/.config/skills/store      a project
(the store)    --->  (local clone)         --->  .claude/skills/
                       skills sync               .agents/skills/
                                                 skills install
  • The store is a Git clone of your skills repository, kept under your config directory. One copy serves every project on the machine.
  • The config says where the store came from and which agents exist.
  • The installs are copies of individual skills inside a project, or in your home directory with --global.

A new store is a partial, sparse clone: it keeps the full commit history but downloads file contents only as needed, and checks out only skills/. Reading a commit never reaches the network, so ls, install, update, diff and dry runs work offline once the content is there.

Layout

~/.config/skills/config.toml     what the store is, which agents exist
~/.config/skills/store/          git clone of the skills repository
~/.config/skills/backups/        timestamped copies taken before --force or remove

XDG_CONFIG_HOME is honoured when set.

commands

init #

Create the config and clone the store
skills init

Writes a default config if none exists and clones the configured branch into the store. Running it is optional: every command that needs the store does this on first use.

The clone is made in a temporary directory and moved into place only after setup succeeds, so a failed first run never leaves a half-made store.

sync #

Fast-forward the configured store branch
skills sync [--dry-run]

Pulls new commits into the store with a fast-forward only. It never creates a merge commit and never resets a dirty or divergent store. It does not touch installed skills, so syncing never rewrites a project by surprise. update is the command that moves installed skills to the synced content.

$ skills sync
  pulled store  a1b2c3d -> e4f5a6b

With --dry-run, sync only queries the remote branch head and reports both heads. It fetches nothing and writes nothing. A remote query failure exits 1 and is never reported as up to date.

ls #

List published skills in the store, or installed skills
skills ls [--local | --global]
flaglists
nonepublished skills in the store, with their tags
--localskills installed at the repository root, with the agents they are installed for
--globalskills installed in the home directories
$ skills ls
  go-review        Reviews Go code for correctness and idiom     go, review
  django-testing   Writes and structures Django tests            python, django

$ skills ls --local
  go-review        Reviews Go code for correctness and idiom     agents, claude

--local and --global cannot be combined and list only the default agents' installs. Drafts never appear in the store listing. Installed listings show the directory name and mark an unreadable description instead of hiding the skill.

install #

Install skills at the repository root
skills install <skill>... [--agent name...] [--global] [--force] [--dry-run] [-v]

Writes a copy of each skill for every selected agent, with a .skill-lock.json beside it. Defaults to the current repository root and the agents listed under defaults.agents in the config.

flageffect
--agentnarrow to certain agents; repeatable
--globalwrite to the home directories instead of the repository
--forceoverwrite skills you have edited or did not install, backed up first
$ skills install demo --agent codex
  + demo   added

  1 skill, 1 changed

An identical unmanaged copy already on disk is adopted by writing its lock. Different unmanaged content needs --force and gets a backup. A name missing from the store is a selection error: all missing names are reported, nothing is written, and the exit code is 1.

Foreign installs stay untouched. If a lock's source differs from the configured store, the skill is left alone and reported as needing attention, even with --force. Run remove and then install to switch it over.

update #

Re-install installed skills from the store
skills update [skill...] [--agent name...] [--global] [--force] [--dry-run] [-v]

With no names, refreshes every managed skill in the selected scope and agents. It updates only targets where a skill is already installed; use install to add a target. A skill installed for a non-default agent needs that agent selected again with --agent.

$ skills update
  ~ go-review        updated
  ! sql-review       skipped, you edited it

  2 skills, 1 changed, 1 needs attention
  Run 'skills diff sql-review' to see your changes,
  or 'skills update sql-review --force' to overwrite (backed up).

Unchanged skills print nothing but count in the summary. -v lists the paths under each skill, which is where the agents become visible:

$ skills update -v
  ~ go-review        updated
      .claude/skills/go-review/SKILL.md
      .agents/skills/go-review/SKILL.md

If an installed skill disappears from the store or becomes a draft, update reports it as unavailable and leaves it installed. Only an explicit remove uninstalls a skill.

remove #

Uninstall skills from the selected scope
skills remove <skill>... [--agent name...] [--global] [--force] [--dry-run] [-v]

No confirmation prompt: typing the command is the confirmation. It acts only on the selected scope and needs --global to remove a global installation. Every successful removal is backed up first and the exact backup path is printed.

$ skills remove demo --agent codex --dry-run
  - demo   would remove
      would back up first

  1 skill, 1 would change

An edited skill is a conflict unless --force is present. Added, modified or deleted tracked content, added local-only files, and changed executable bits all count as edits. A valid lock is enough to attempt removal even if SKILL.md is missing or malformed.

diff #

Show your edits to an installed skill
skills diff <skill> [--agent name...] [--global]

Compares each selected installation against the commit recorded in its lock, using the same agent transform. It compares against what was installed, not the latest store head, so the output is exactly your edits.

Additions, deletions and malformed local frontmatter are shown as content. Executable-bit changes are shown on POSIX systems. No differences means no output. Both outcomes exit 0.

A missing recorded commit or a foreign source produces an explanation and exit 1. The CLI never substitutes a different base. If a fresh sparse store lacks the recorded commit's content, the error names the commit and gives the git archive command that downloads it.

version #

Show the CLI version and the current store commit
skills version

The CLI version and the store commit are separate facts, and this prints both. An absent store is reported, not created.

Global flags #

flagmeaningapplies to
--globalact on the home directories instead of the repositoryinstall, update, remove, diff, ls
--agent <name>...narrow to certain agents (default: config defaults)install, update, remove, diff
--forceoverwrite skills you have edited, backed up firstinstall, update, remove
--dry-runshow the plan, write nothingevery command
-v, --verboseshow individual files and their targetsevery command
--no-colordisable color; NO_COLOR is honoured tooevery command

--dry-run never creates the config, clones the store, writes a backup or creates a target directory. If a plan cannot be calculated because the store is not initialised, it says so and points at skills init.

reference

Scope and agents #

Project scope means the root of the current Git repository, no matter which subdirectory you run from. Outside a repository the CLI warns and uses the current directory. --global uses your home directories instead. Nothing else changes: same skill, same lock, different parent path.

agentprojectglobal
Claude Code.claude/skills/~/.claude/skills/
Codex.agents/skills/~/.agents/skills/
Gemini CLI.agents/skills/ (reads the Codex directory)~/.agents/skills/

Gemini CLI reads .agents/skills/ too, so --agent codex already covers it. To select it by name, add an [agents.gemini] entry with the same paths.

Scope is always explicit and never falls back. If a name is missing locally but present in the global directories, update, remove and diff print a --global hint. The hint never changes the scope or performs the operation.

Configuration #

The first run writes this file to ~/.config/skills/config.toml. Every project on the machine shares it.

[store]
repo = "https://github.com/madalinpopa/skills"
branch = "main"

# Which agents exist, and where each one keeps its skills.
[agents.claude]
project = ".claude/skills"
global  = "~/.claude/skills"

[agents.codex]
project = ".agents/skills"
global  = "~/.agents/skills"

[defaults]
agents = ["claude", "codex"]
  • The agent name claude selects the Claude transform. Every other name uses the shared transform, so a custom alias can reuse the Codex format.
  • defaults.agents is used when --agent is absent. It must be nonempty and name defined agents. An explicit --agent may select any configured name.
  • Project paths must be relative and stay inside the project root. Global paths must be absolute or start with ~/.
  • Several agents may share one directory. Installs are de-duplicated by resolved path, so the files are written once.

Your own store #

You do not need to fork anything. Create a Git repository with a skills/ folder, one folder per skill.

skills/
  go-review/
    SKILL.md              the skill itself
    agents/openai.yaml    optional, copied only for Codex
    references/           optional, copied for every agent

The folder name is the skill name. The CLI reads only committed files under skills/, at the commit it installs from. Uncommitted or untracked files are never installed. The rest of the repository can hold anything.

Point the CLI at it

[store]
repo = "https://github.com/your-name/your-skills"
branch = "main"

Any URL that git clone accepts works, including SSH and private repositories. The CLI reuses your existing Git credentials.

Switching from another repository

If you already ran the CLI with a different store, move the old one aside first. It may hold local work you want to keep.

$ mv ~/.config/skills/store ~/.config/skills/store.old
$ skills init

Skills installed from the old repository stay as they are. install and update skip them, even with --force. To move one over, run skills remove and then skills install.

Publishing

Commit and push a skill to your branch, then run skills sync. A skill with status: draft in its frontmatter is invisible and cannot be installed. A skill with no status is published. Changing a skill never needs a new CLI release.

Skill metadata #

One SKILL.md serves every agent. Its frontmatter may carry three fields the agents never see; the CLI resolves them on install. All three are optional, so a standard Agent Skills file installs without them.

---
name: go-review
description: Reviews Go code for correctness and idiom. Use when...
status: published
tags: [go, review, testing]
x-claude:
  disable-model-invocation: true
  allowed-tools: Bash(go test *)
---

# Go review
...
fieldpurpose
statuspublished or draft. Omitted means published. A draft is invisible to ls and cannot be installed.
tagsa list of strings shown by ls. Omitted or empty means no tags.
x-claudefrontmatter only Claude should see. Lifted to the top level for Claude, dropped for everyone else.

What install writes

.claude/skills/.agents/skills/
status, tagsstrippedstripped
x-claudelifted to the top leveldropped
everything elsepassed throughpassed through
agents/openai.yamlnot copiedcopied
other filescopiedcopied

The CLI requires a nonempty name and description, and the name must match the folder. Unknown fields pass through untouched. Standard fields such as license, compatibility, metadata and allowed-tools stay at the top level. Claude-only fields such as model, effort or hooks belong under x-claude if you want to keep them out of the shared copy.

Two names, one skill. Claude Code takes the command name from the directory, while Codex and Gemini take it from the name field. Keep them identical or one skill answers to two names.

How install and update decide #

For every file of every skill, the CLI compares three states: content hash and, on POSIX, the executable flag.

statemeaning
wantwhat the store holds for that agent, with store-only fields stripped
havewhat is on disk now
basewhat the CLI last wrote, recorded in .skill-lock.json
conditionresult
wanted by the store, not on diskadd
have equals wantnothing to do
have equals baseupdate, you never touched it
anything elseconflict, this is your edit

The first row deliberately restores a deleted tracked file if the store still has it. Files you added yourself are never tracked and never removed. A skill is the unit of conflict checking: one conflict skips the whole skill across all its agent targets, and other skills proceed.

The per-skill lock

{
  "name": "go-review",
  "source": "https://github.com/madalinpopa/skills",
  "commit": "a1b2c3d4e5f678901234567890abcdef12345678",
  "installed": "2026-09-12T18:40:00Z",
  "files": { "SKILL.md": "<sha256>" },
  "executable": { "SKILL.md": false }
}

There is no central index. Installed skills are found by scanning the agents' directories for this file. Delete the skill directory and the state goes with it. Copy it to another project and the record goes along.

Conflicts and backups #

A skill you edited is never overwritten silently. The CLI skips the whole skill, says so, carries on with the others, and exits with code 3 so scripts can tell partial completion from success.

--force overwrites only after copying the old content into a timestamped directory under ~/.config/skills/backups/. Enough of the target path is preserved to tell where the backup came from, and the exact path is printed. Backups never overwrite an earlier backup, even two in the same second.

Force is not a bypass. It resolves content conflicts using the store version. It does not touch malformed locks, skills from another source, or symlinks and other unsupported files inside a skill. Those need manual attention.

Output and exit codes #

One line per skill, not per file. A skill installed for two agents prints one line. Symbols carry the meaning, so nothing is lost when color is off.

+ added
~ updated
- removed
! needs attention

Dry-run lines read would add, would update and would remove, and the summary says N would change. Results go to stdout; warnings and diagnostics go to stderr. Color is used only when stdout is a terminal, and NO_COLOR disables it.

codemeaning
0every requested operation completed
1runtime error, or a requested name was not found
2invalid command, flag or agent name
3attention required: a skill was skipped or installed state needs repair

A runtime error takes precedence over attention. The full behavior contract lives in docs/SPEC.md.