Loom documentation
Loom is a package manager for AI skills. It installs, inspects, and authors reusable skills for AI agents such as Claude Code, OpenAI Codex, and Cursor.
A skill is a folder of instructions and helper files an agent loads to do a job well — writing PDFs, building slide decks, designing artifacts, and so on. Loom does for those skills what Homebrew does for command-line tools: a shared repository of manifests describes where each skill lives and how to fetch it, and Loom weaves the payload into your agent's skills folder on request.
A manifest never contains the skill payload itself. It only describes how to obtain and place a skill, in the spirit of a Homebrew formula. That keeps the repository small and lets skills live in their own upstream projects, maintained by the people who wrote them.
Concepts
Four nouns carry the whole system. If the mental model is right, every command follows from it.
| Term | What it is |
|---|---|
skill |
A folder with a SKILL.md at its root, plus whatever helper files it needs. Lives in its author's own git repository. This is the payload. |
manifest |
A YAML file, <name>.yml, describing where a skill lives and how to install it. The Homebrew analogue is a formula. Contains no payload. |
repository |
A folder of manifests — the skills/ directory of wess/loom. The Homebrew analogue is a tap. Loom keeps a synced clone of it locally. |
agent |
An AI tool that loads skills from a directory. Loom ships with three, and you can add your own. The install target. |
The flow from recipe to installed skill:
skills/pdf.yml loom install pdf ~/.claude/skills/pdf/
the recipe ───▶ fetch + verify ───▶ the payload
name, source URL, clone at the pinned SKILL.md and its
ref, subdir ref, check the entry helper files
Installation
Loom is a single Rust binary. Pick whichever route suits you.
Homebrew (macOS & Linux)
brew install wess/packages/loom
Install script (macOS & Linux)
curl -fsSL https://raw.githubusercontent.com/wess/loom/main/scripts/install.sh | sh
It detects your OS and architecture, downloads the matching
release, verifies its checksum, and drops loom on your
PATH. Set LOOM_VERSION to pin a version,
or LOOM_BIN_DIR to choose the install directory.
Prebuilt binaries
Grab a .tar.gz for your platform from the
releases page
— builds are published for macOS and Linux on both
x86_64 and aarch64 — then extract
loom onto your PATH.
From source
Needs a Rust toolchain; get one from rustup.rs.
cargo install --git https://github.com/wess/loom
Or clone first, to read the source or work on it:
git clone https://github.com/wess/loom
cd loom
cargo install --path .
cargo install loom
That command does not install this tool. The
loom name on crates.io belongs to an unrelated
concurrency-testing library with no binary, so it fails with
there is nothing to install. This crate is named
loom_ai — once it is published,
cargo install loom_ai will put a loom
binary on your PATH. Until then, use one of the routes
above.
Requirements
| Dependency | Needed for |
|---|---|
git | Every git source. Loom shells out to the system git, exactly as Homebrew does. Must be on PATH. |
| Network access | install, update, upgrade, test, and generate. |
gh | loom publish --execute only. Everything else works without the GitHub CLI. |
| A terminal | loom init only — it refuses to run when stdin is not a TTY. |
Confirm the install and check the environment in one step:
loom --version
loom doctor
loom doctor verifies that git is
reachable, that Loom's home directory is writable, that
config.json parses, and that the manifest repository
can be read. It exits successfully even when it finds problems —
read its output, do not just check the exit code.
Quickstart
Sync the repository first. Loom searches a local clone of it, and until that clone exists there is nothing to search.
# sync the manifest repository (do this first)
loom update
# search the repository
loom search pdf
# read the details before installing
loom info pdf
# install into the default agent (Claude Code)
loom install pdf
# see what's installed
loom list
loom update first
Before it has synced, Loom looks for manifests in
./skills relative to your current directory. Running
loom search from anywhere else fails with
manifest directory not found. One
loom update fixes it permanently by recording a
repo_path in your config.
By default Loom installs into Claude Code at
~/.claude/skills. Target a different agent with
--agent:
loom install pdf --agent codex
Keep everything current by refreshing the recipes, then acting on them:
loom update # pull the newest manifests
loom upgrade # reinstall skills whose pinned version moved
CLI reference
Every command Loom ships with. Run
loom <command> --help for full flags. Loom exits
non-zero and prints an error: line to stderr when a
command fails.
Consuming skills
| Command | What it does |
|---|---|
loom update |
Clone or fast-forward the manifest repository from its configured repo_url into ~/.loom/cache/repo. Records the result as repo_path if you have not set one. Refreshes recipes only; installed skills are untouched. |
loom search <query> |
Rank repository manifests against a query. An exact name match scores highest, then a name prefix, then a keyword hit, then a substring of the description. Prints an aligned table. |
loom info <skill> |
Print a skill's full manifest — source, subdir, license, authors — plus which agents currently have it installed. |
loom install <skill> [-a, --agent <id>] |
Fetch and install a skill. <skill> is a repository skill name or a path to a local .yml file. Always reinstalls, replacing any existing copy. Defaults to your default_agent. |
loom list [-a, --agent <id>] |
List installed skills from the registry. Alias: ls. Filter to one agent with --agent. |
loom upgrade [skill] [-a, --agent <id>] |
Reinstall installed skills whose repository version differs from the installed one. Omit the name to consider everything. Skips skills no longer in the repository, with a warning. |
loom remove <skill> [-a, --agent <id>] |
Delete an installed skill's directory and drop it from the registry. Aliases: uninstall, rm. |
Authoring skills
| Command | What it does |
|---|---|
loom new <name> [-o, --out <dir>] |
Write a bare manifest template. Fails if the file already exists. Defaults to your configured repository folder — pass --out . to write to the current directory. |
loom init [--url <url>] [-r, --ref <ref>] [-o, --out <dir>] |
Author a manifest through an interactive wizard, validating each answer. --url pre-fills the answers by inspecting a real skills repository. Offers to test-fetch the result. Requires a terminal. |
loom lint [path] |
Validate one manifest file, or every manifest in the configured repository when no path is given. Exits non-zero if any manifest has an error. Warnings never fail the command. |
loom test <skill> |
Fetch a skill into a scratch directory, count its files, and confirm the entry file exists — then delete the scratch. Proves a manifest resolves without touching any agent. Accepts a name or a manifest path. |
loom generate <url> [-r, --ref <ref>] [-o, --out <dir>] |
Clone a skills repository, find every folder containing a SKILL.md, and emit a draft manifest for each. Prints YAML to stdout unless --out is given. Alias: create. The URL may also be passed as --url. |
loom publish <skill> [--repo <url>] [--execute] |
Fork the repository, commit your manifest to a loom/<name> branch, and open a pull request via gh. Prints the plan and changes nothing unless --execute is passed. Re-validates the manifest first. |
Environment
| Command | What it does |
|---|---|
loom agents | List the agents Loom can install into, with their resolved skills folders. A * marks the default. |
loom doctor | Check for git, a writable home, a parseable config, and a readable repository. Reports how many manifests it found, and how many are broken. |
loom index [-o, --out <path>] | Build the website's JSON search index from the repository. Defaults to docs/skills.json. |
install, test, and publish
accept either a repository skill name or a path to a local
.yml file. They do not fetch a manifest from
a URL — download it first, then pass the path.
Manifest format
A manifest is a YAML file named <skill_name>.yml
(or .yaml) that lives in a Loom repository's
skills/ folder. It describes how to fetch and lay down
exactly one skill.
name: pdf # unique id, lowercase, no whitespace
version: 1.0.0 # the version this manifest pins
description: Read, fill, and generate PDF documents.
homepage: https://github.com/anthropics/skills
license: MIT
authors:
- Anthropic <support@anthropic.com>
keywords:
- pdf
- documents
compatibility:
- claude-code
- codex
source:
type: git # git | archive
url: https://github.com/anthropics/skills
ref: v1.0.0 # git tag, branch, or commit (git sources)
# sha256: <hex> # required for archive sources
subdir: skills/pdf # folder inside the source that is the skill
install:
entry: SKILL.md # the file an agent loads first
files: [] # explicit paths to include; empty copies everything
Field reference
| Field | Required | Meaning |
|---|---|---|
name | yes | Unique skill identifier. Must be lowercase and contain no whitespace. Becomes the installed folder name. |
version | yes | Semver-ish string. Compared as text by upgrade to decide whether to reinstall. |
description | yes | One line, shown in search results and on the website. Over 500 characters earns a warning. |
homepage | yes | Project homepage. Must start with http. |
license | no | SPDX identifier, e.g. MIT. Omitted from the file when unset. |
authors[] | no | Free-form Name <email> strings. An empty list earns a warning. |
keywords[] | no | Search tags. A keyword hit outranks a description hit, so these meaningfully improve discovery. |
compatibility[] | no | Agent ids the skill is known to work with. Advisory only — it never restricts where Loom installs. |
source.type | yes | git to clone a repository, or archive to download a .tar.gz. |
source.url | yes | Clone URL (git) or tarball URL (archive). Must start with http or git. |
source.ref | git | Tag, branch, or commit to check out. Omitting it earns a warning — an unpinned skill is not reproducible. |
source.sha256 | archive | Hex checksum the download is verified against. An archive source without one is an error. |
source.subdir | no | Path inside the source that holds the skill. Omit when the repository root is the skill. |
install.entry | no | The file an agent loads first, and the one Loom checks for after copying. Defaults to SKILL.md. |
install.files[] | no | Explicit relative file paths to copy. Empty copies the whole subdir. Lists individual files, not directories. |
Sources
Git sources are shallow-cloned with the system
git at the pinned ref. Because a shallow
clone cannot check out an arbitrary commit by branch name, Loom
silently retries with a full clone and a git checkout
when the ref is a commit SHA. Tags and branches take the fast path.
source:
type: git
url: https://github.com/you/skills
ref: v1.2.0 # tag and branch: fast. commit SHA: slower fallback.
subdir: changelog
Archive sources are downloaded over HTTPS,
verified against sha256, and unpacked as a gzip
tarball. Many tarballs wrap their contents in a single top-level
directory; Loom descends into it automatically, so
subdir is always relative to the real project root.
source:
type: archive
url: https://example.com/skills-1.2.0.tar.gz
sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
subdir: changelog
Compute the checksum before you commit the manifest:
curl -sL https://example.com/skills-1.2.0.tar.gz | shasum -a 256
ref is a warning; an archive
source without a sha256 is a hard error and will not
install. A tarball URL can change contents underneath you, and the
checksum is the only thing standing in the way.
What install does on disk
- Validate the manifest. Hard errors stop here, before any network access.
- Fetch the source into
~/.loom/cache/build/<name>/. - Resolve
subdiragainst the checkout. A missing subdir is an error. - Delete
<agent skills dir>/<name>/if it exists, then recreate it. - Copy the payload in — the whole subdir, skipping
.git, or just the paths ininstall.files. - Confirm
install.entryexists in the target. A missing entry file fails the install. - Record the install in
~/.loom/state.jsonand clean up the scratch directory.
Lint rules
loom lint separates errors, which
make a manifest uninstallable, from warnings,
which are advisory quality nits. Only errors fail the command, and
only errors block an install or a publish.
| Severity | Rule |
|---|---|
| error | name is empty, not lowercase, or contains whitespace |
| error | version or description is empty |
| error | homepage does not start with http |
| error | source.url does not start with http or git |
| error | install.entry is empty |
| error | an archive source declares no sha256 |
| warn | description is longer than 500 characters |
| warn | no authors are listed |
| warn | a git source has no ref — the install is not reproducible |
Lint is offline and instant. It cannot tell you whether a
subdir exists or an entry file is really there — that
is what loom test is for.
Agents
An agent is a name, a label, and a directory. Loom ships with three and treats the first as the default:
| Id | Label | Skills directory |
|---|---|---|
claude-code | Claude Code (default) | ~/.claude/skills |
codex | OpenAI Codex | ~/.codex/skills |
cursor | Cursor | ~/.cursor/skills |
Run loom agents to see them with their resolved paths.
The same skill may be installed into several agents at once; each
install is tracked separately, so you can upgrade or remove one
without disturbing the others.
compatibility list records which agents
the author tested. It does not gate anything. The install target
comes from --agent, falling back to
default_agent.
Teach Loom about a new agent by adding it to
~/.loom/config.json. The id becomes a valid
--agent value immediately — there is nothing to
register or rebuild.
Configuration & state
Everything Loom owns lives under a single prefix,
~/.loom, mirroring the way Homebrew keeps a
self-contained one. Override it with the LOOM_HOME
environment variable — useful for trying Loom without touching your
real setup, and for isolating it in CI.
~/.loom/
config.json agents, default agent, repository settings
state.json the registry of installed skills
cache/
repo/ the synced clone of the manifest repository
build/ scratch dirs used during install and test
generate/ scratch clones used by `loom generate`
LOOM_HOME=/tmp/loom-sandbox loom doctor
config.json
Written with defaults the first time any command runs. Edit it freely; Loom re-reads it every invocation.
{
"agents": {
"claude-code": { "label": "Claude Code", "skills_dir": "~/.claude/skills" },
"codex": { "label": "OpenAI Codex", "skills_dir": "~/.codex/skills" },
"cursor": { "label": "Cursor", "skills_dir": "~/.cursor/skills" }
},
"default_agent": "claude-code",
"repo_path": null,
"repo_url": "https://github.com/wess/loom"
}
| Key | Meaning |
|---|---|
agents | Map of agent id to { label, skills_dir }. A leading ~/ in skills_dir expands to your home directory. |
default_agent | The agent used when --agent is not given. |
repo_path | Local folder of manifests Loom reads. loom update sets it. When null, Loom falls back to ./skills relative to your current directory. |
repo_url | Git remote loom update syncs from, and the default target for loom publish. |
repo_path fallback surprises people
With repo_path unset, loom search,
lint, new, and init all
resolve against ./skills in whatever directory you
happen to be. This is convenient when you are working inside a
clone of the Loom repository and confusing everywhere else. Run
loom update once to pin it down.
state.json
The registry of what is installed where. list,
upgrade, and remove all read it, which is
why removal only ever deletes a path Loom itself recorded.
{
"installed": [
{
"name": "pdf",
"version": "0.1.0",
"agent": "claude-code",
"path": "/Users/you/.claude/skills/pdf",
"source_url": "https://github.com/anthropics/skills"
}
]
}
Deleting a skill folder by hand leaves a stale record behind.
loom remove tolerates that — it drops the record even
when the path is already gone.
Publishing
loom publish automates the fork-branch-commit-PR dance
through the GitHub CLI. It defaults to a dry run
that prints its plan and changes nothing:
loom publish my-skill.yml
output
==> Publishing my-skill 0.1.0 to wess/loom
→ fork wess/loom to your account (if needed)
→ clone your fork and branch loom/my-skill
→ copy my-skill.yml to skills/my-skill.yml
→ commit "Add my-skill 0.1.0" and push
→ open a PR into wess/loom
warning: dry run — nothing was changed. re-run with --execute to open the PR.
Authenticate gh once, then run it for real:
gh auth login
loom publish my-skill.yml --execute
The manifest is re-validated before anything is pushed, so a
manifest with a hard error can never reach a pull request. Point at
a different upstream with --repo <url> if you
maintain your own collection.
.yml into skills/, and open the
pull request by hand — the result is identical, and you need no
gh.
Importing a repo
Already have a repository full of skills?
loom generate clones it, finds every folder containing
a SKILL.md, reads its front matter, and drafts a
manifest for each.
# print the drafts to stdout, touching nothing
loom generate https://github.com/you/skills --ref v1.0.0
# or write one file per discovered skill
loom generate https://github.com/you/skills --ref v1.0.0 --out skills/
Generation infers what it can:
| Field | Inferred from |
|---|---|
name | The name in SKILL.md front matter, else the folder name. A SKILL.md at the repo root takes the repository's name. |
description, keywords | Front matter. Both keywords and tags are read, as a list or a comma-separated string. Folded and literal scalars resolve correctly. |
version | --ref, when it looks like semver — v1.2.0 becomes 1.2.0. A branch name such as main falls back to 0.1.0. |
license | A LICENSE, LICENSE.md, LICENSE.txt, or COPYING file at the repo root, matched to an SPDX id. |
authors | The repository owner parsed out of the URL. |
subdir | The discovered folder's path, relative to the repo root. Searched to a depth of four. |
These are drafts. Read each one — a description that reads well
inside a SKILL.md is often far too long for a search
result — then loom lint and open a PR.
create is an alias for the same command.
The website index
This site's search is powered by a generated index at
docs/skills.json, produced by reading every manifest
in the repository's skills/ folder:
loom index # defaults to docs/skills.json
loom index --out docs/skills.json # or name the destination
The output is a small JSON document the Browse page fetches at runtime:
{
"generated_by": "loom 0.1.0",
"count": 287,
"skills": [
{
"name": "pdf",
"version": "0.1.0",
"description": "Read, fill, and generate PDF documents.",
"homepage": "https://github.com/anthropics/skills",
"license": null,
"authors": ["anthropics"],
"keywords": [],
"compatibility": ["claude-code"],
"source": "https://github.com/anthropics/skills"
}
]
}
docs/skills.json
and commits it back whenever a manifest lands on main.
Pull-request CI only checks that the index builds. Run
loom index yourself only when you want to preview the
site locally before merge.
The docs/ folder is a dependency-free static site with
no build step, served as a project page under
wess.io/loom/. Every internal link is relative for
that reason.
Troubleshooting
| Message | Cause and fix |
|---|---|
manifest directory not found |
You have not synced the repository, so Loom is looking in ./skills relative to your current directory. Run loom update. |
skill '<name>' not found in … |
No <name>.yml in the repository. Check the spelling with loom search, or run loom update if the skill was added recently. Note that a URL is not a valid skill argument — pass a name or a local file path. |
source subdir not found: … |
source.subdir does not exist inside the fetched source. Check the path against the repository at the pinned ref — the folder may have moved since the tag you named. |
entry file 'SKILL.md' missing after install |
The subdir resolved, but held no entry file. Either subdir points one level too high, or install.files omits the entry file. |
git ref not found: <ref> |
The tag, branch, or commit in source.ref does not exist upstream. Tags are the safest thing to pin. |
sha256 mismatch |
The archive's contents changed since the manifest was written. Do not "fix" this by pasting in the new hash without checking what changed — verify the tarball first. |
archive source must declare a sha256 |
Every type: archive source needs a checksum. Compute it with curl -sL <url> | shasum -a 256. |
'<name>' is not installed for agent '<id>' |
Nothing in the registry matches. loom list shows what is actually installed and for which agent — you may have installed it for a different one. |
unknown agent '<id>' |
The --agent value is not a key in config.json. loom agents lists the valid ids. |
loom init is interactive and needs a terminal |
stdin is not a TTY. In scripts and CI, use loom new <name> or loom generate <url>. |
not logged in to GitHub |
loom publish --execute needs an authenticated gh. Run gh auth login. |
there is nothing to install in `loom v0.7.2` |
You ran cargo install loom and got an unrelated crates.io library. This crate is loom_ai. See Installation. |
When something is wrong and the message does not say what, start
with loom doctor, then loom info <skill>
to see exactly which manifest Loom resolved and where it came from.
FAQ
Does the repository host the skills themselves?
No. It holds manifests only — a few hundred bytes of YAML each. Skills live in their authors' repositories and are fetched at install time. Contributing a skill never means uploading a payload.
Can I install a skill from a private repo?
Yes, if your git can already clone it. Loom shells out
to the system git, so it inherits your SSH keys and
credential helper. Use an SSH URL in source.url.
Can I install a manifest without publishing it?
Yes. Anywhere a skill name is accepted, a path to a local
.yml works too: loom install ./my-skill.yml.
This is how you should test a manifest before opening a PR.
Why does loom upgrade say a skill is up to date when upstream changed?
Upgrade compares the manifest's version string against
what you installed. A manifest pinned to a moving ref like
main keeps the same version while its upstream
advances, so upgrade sees no change. Force a refetch with
loom install <skill>, which always reinstalls.
Can I run my own repository of manifests?
Yes. Point repo_url at your own git repository, or set
repo_path to a local folder of .yml files.
loom publish --repo <url> targets a different
upstream for a single command.
How do I remove Loom entirely?
Delete the binary (cargo uninstall loom) and its state
directory (rm -rf ~/.loom). Installed skills are plain
folders in your agents' skills directories — remove them with
loom remove beforehand, or delete them by hand
afterwards.