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.
init #
Create the config and clone the storeWrites 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 branchPulls 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| flag | lists |
|---|---|
| none | published skills in the store, with their tags |
--local | skills installed at the repository root, with the agents they are installed for |
--global | skills 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 rootWrites 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.
| flag | effect |
|---|---|
--agent | narrow to certain agents; repeatable |
--global | write to the home directories instead of the repository |
--force | overwrite 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 storeWith 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 scopeNo 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 skillCompares 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 commitThe CLI version and the store commit are separate facts, and this prints both. An absent store is reported, not created.
Global flags #
| flag | meaning | applies to |
|---|---|---|
--global | act on the home directories instead of the repository | install, update, remove, diff, ls |
--agent <name>... | narrow to certain agents (default: config defaults) | install, update, remove, diff |
--force | overwrite skills you have edited, backed up first | install, update, remove |
--dry-run | show the plan, write nothing | every command |
-v, --verbose | show individual files and their targets | every command |
--no-color | disable color; NO_COLOR is honoured too | every 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.
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.
| agent | project | global |
|---|---|---|
| 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
claudeselects the Claude transform. Every other name uses the shared transform, so a custom alias can reuse the Codex format. defaults.agentsis used when--agentis absent. It must be nonempty and name defined agents. An explicit--agentmay 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 agentThe 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 ...
| field | purpose |
|---|---|
status | published or draft. Omitted means published. A draft is invisible to ls and cannot be installed. |
tags | a list of strings shown by ls. Omitted or empty means no tags. |
x-claude | frontmatter only Claude should see. Lifted to the top level for Claude, dropped for everyone else. |
What install writes
.claude/skills/ | .agents/skills/ | |
|---|---|---|
status, tags | stripped | stripped |
x-claude | lifted to the top level | dropped |
| everything else | passed through | passed through |
agents/openai.yaml | not copied | copied |
| other files | copied | copied |
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.
| state | meaning |
|---|---|
| want | what the store holds for that agent, with store-only fields stripped |
| have | what is on disk now |
| base | what the CLI last wrote, recorded in .skill-lock.json |
| condition | result |
|---|---|
| wanted by the store, not on disk | add |
have equals want | nothing to do |
have equals base | update, you never touched it |
| anything else | conflict, 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.
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.
| code | meaning |
|---|---|
| 0 | every requested operation completed |
| 1 | runtime error, or a requested name was not found |
| 2 | invalid command, flag or agent name |
| 3 | attention 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.