Contributing
This is a personal registry first. Pull requests for genuinely reusable bundles are still welcome.
Add a bundle
- Fork the repository and create
skills/<slug>/(see Bundle structure). - Write
README.mdwith valid frontmatter and a body that explains what the bundle does and how to install it. - Run the validator locally:
pnpm install pnpm validate:skills - Run the site against your working tree and check the page:
pnpm dev # open http://localhost:3000/skill/<slug> - Open a pull request. CI runs the same validator plus the app's lint, typecheck, tests and build.
What happens when it merges
Bundle content is read from GitHub at runtime. Merging to main:
- skips the Vercel build when only
skills/**changed (the Ignored Build Step invercel.json); - runs the
revalidateworkflow, which purges the cache tag and asserts the site now serves your commit.
Your bundle is live within seconds of the merge.
Files a bundle must not own
The habits below are what keep a bundle composable with the others.
Read configuration from .claude/.env, never the repo root .env. That separation is what
lets a project hand Claude a read-only database replica while the app keeps its own connection
string. Declare each variable in the env frontmatter and let the
tool write .claude/.env.example.
set -a; . "$CLAUDE_PROJECT_DIR/.claude/.env"; set +a
process.loadEnvFile('.claude/.env')
Python has no .env reader in its standard library, and a skill that adds python-dotenv for a
dozen lines hands every user of it a pip install. Parse the file:
import os
from pathlib import Path
def load_claude_env(path=Path(__file__).resolve().parents[2] / '.env'):
if not path.is_file():
return
for line in path.read_text().splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
name, sep, value = line.partition('=')
if not sep:
continue
value = value.strip()
if len(value) > 1 and value[0] == value[-1] and value[0] in '"\'':
value = value[1:-1]
os.environ.setdefault(name.strip(), value)
setdefault, not assignment, so a value already exported in the shell wins. Where a skill has
dependencies anyway, python-dotenv reads the same file:
from dotenv import dotenv_values
config = dotenv_values(Path(__file__).resolve().parents[2] / '.env')
Either way the path walks up from .claude/skills/<name>/script.py to .claude/, so a script one
level deeper needs parents[3]. Take an explicit path argument if it can be run from anywhere.
Cache under your own skill directory, and declare it. A skill that downloads or generates
anything writes it beside its own SKILL.md, so removing the skill removes the cache. Put the path
in the gitignore frontmatter and the tool adds it to the project's managed
block. The site never zips or serves a cache/ directory anyway, so a committed cache only adds
weight to the repo.
Never ship a .env.example file. The project gets exactly one, assembled from every installed
bundle, and a bundle that ships its own fails validation with a pointer to the env key.
What gets a pull request sent back
- No secrets, credentials, internal hostnames or IPs. Bundles are public.
- No generated caches.
cache/directories are ignored anyway, but do not commit them. - Keep bundles generic. Project-specific paths, theme mappings or account names belong in the user's own
CLAUDE.md, not here. - Lowercase tags, one concern per bundle.
- READMEs render as markdown with raw HTML allowed (scripts and event handlers are stripped), so bundle content is trusted at PR-review time. Review it as you would any other code you merge.