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.

New to Loom? The tutorial walks from an empty machine to a published skill in about twenty minutes. This page is the reference you come back to afterwards.

Concepts

Four nouns carry the whole system. If the mental model is right, every command follows from it.

TermWhat 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 .
Not 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

DependencyNeeded for
gitEvery git source. Loom shells out to the system git, exactly as Homebrew does. Must be on PATH.
Network accessinstall, update, upgrade, test, and generate.
ghloom publish --execute only. Everything else works without the GitHub CLI.
A terminalloom 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
Run 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

CommandWhat 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

CommandWhat 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

CommandWhat it does
loom agentsList the agents Loom can install into, with their resolved skills folders. A * marks the default.
loom doctorCheck 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.
Skill arguments 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

FieldRequiredMeaning
nameyesUnique skill identifier. Must be lowercase and contain no whitespace. Becomes the installed folder name.
versionyesSemver-ish string. Compared as text by upgrade to decide whether to reinstall.
descriptionyesOne line, shown in search results and on the website. Over 500 characters earns a warning.
homepageyesProject homepage. Must start with http.
licensenoSPDX identifier, e.g. MIT. Omitted from the file when unset.
authors[]noFree-form Name <email> strings. An empty list earns a warning.
keywords[]noSearch tags. A keyword hit outranks a description hit, so these meaningfully improve discovery.
compatibility[]noAgent ids the skill is known to work with. Advisory only — it never restricts where Loom installs.
source.typeyesgit to clone a repository, or archive to download a .tar.gz.
source.urlyesClone URL (git) or tarball URL (archive). Must start with http or git.
source.refgitTag, branch, or commit to check out. Omitting it earns a warning — an unpinned skill is not reproducible.
source.sha256archiveHex checksum the download is verified against. An archive source without one is an error.
source.subdirnoPath inside the source that holds the skill. Omit when the repository root is the skill.
install.entrynoThe file an agent loads first, and the one Loom checks for after copying. Defaults to SKILL.md.
install.files[]noExplicit 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
Archives must be pinned A git source without a 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

  1. Validate the manifest. Hard errors stop here, before any network access.
  2. Fetch the source into ~/.loom/cache/build/<name>/.
  3. Resolve subdir against the checkout. A missing subdir is an error.
  4. Delete <agent skills dir>/<name>/ if it exists, then recreate it.
  5. Copy the payload in — the whole subdir, skipping .git, or just the paths in install.files.
  6. Confirm install.entry exists in the target. A missing entry file fails the install.
  7. Record the install in ~/.loom/state.json and clean up the scratch directory.
Installs replace, never merge Step 4 wipes the target directory. Reinstalling is a reliable way to reset a skill to its pristine state, and a good reason never to hand-edit files inside an installed skill folder.

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.

SeverityRule
errorname is empty, not lowercase, or contains whitespace
errorversion or description is empty
errorhomepage does not start with http
errorsource.url does not start with http or git
errorinstall.entry is empty
erroran archive source declares no sha256
warndescription is longer than 500 characters
warnno authors are listed
warna 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:

IdLabelSkills directory
claude-codeClaude Code (default)~/.claude/skills
codexOpenAI Codex~/.codex/skills
cursorCursor~/.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 is documentation A manifest's 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"
}
KeyMeaning
agentsMap of agent id to { label, skills_dir }. A leading ~/ in skills_dir expands to your home directory.
default_agentThe agent used when --agent is not given.
repo_pathLocal folder of manifests Loom reads. loom update sets it. When null, Loom falls back to ./skills relative to your current directory.
repo_urlGit remote loom update syncs from, and the default target for loom publish.
The 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.

Authoring a skill

Contributing a skill means adding a manifest to the repository. The payload stays in your own repo, where you maintain it.

  1. Write the skill. A folder with a SKILL.md at its root, pushed to a git repository and tagged.
  2. Author a manifest with loom init (interactive) or loom new (bare template).
  3. Point it at the payloadsource.url and source.subdir together must name the directory holding your SKILL.md.
  4. Lint it to catch missing or malformed fields, offline.
  5. Test it — Loom fetches and stages the payload without installing, proving the source resolves.
  6. Publish it as a pull request against the repository's skills/ folder.
loom init --url https://github.com/you/skills --ref v1.0.0 --out .
loom lint my-skill.yml       # offline field validation
loom test my-skill.yml       # does it actually fetch?
loom install ./my-skill.yml  # try it in your own agent first
loom publish my-skill.yml    # dry run; add --execute to open the PR

loom init walks you through every field, validating as you type, and can pre-fill its answers by inspecting an existing repository. It reads SKILL.md front matter for the name, description, and keywords, sniffs a LICENSE file for an SPDX id, and derives the author from the repo owner.

Mind where manifests are written With no --out, init and new write into your configured repository folder — which, after loom update, is the synced clone under ~/.loom/cache/, where the next update may overwrite them. Pass --out . when authoring.

What makes a good skill

  • The description is the interface. It is what an agent matches against when deciding whether to load the skill, and what a human reads in search results. Say what the skill does and when to reach for it.
  • Pin a tag, not a branch. A manifest pointing at main installs something different every week. Tag a release and pin the tag.
  • Keep the entry file small. SKILL.md is loaded up front. Push detail into sibling files the skill references, and the agent will read them on demand.
  • Bump version when the payload moves. loom upgrade compares version strings. A new tag behind an unchanged version is invisible to it.

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.

Publishing is optional It is a convenience wrapper, not a requirement. Fork the repository, copy your .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:

FieldInferred from
nameThe name in SKILL.md front matter, else the folder name. A SKILL.md at the repo root takes the repository's name.
description, keywordsFront 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.
licenseA LICENSE, LICENSE.md, LICENSE.txt, or COPYING file at the repo root, matched to an SPDX id.
authorsThe repository owner parsed out of the URL.
subdirThe 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"
    }
  ]
}
Contributors never regenerate this A GitHub Actions workflow rebuilds 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

MessageCause 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.