Imported from davehague/skill-librarian (
skill-librarian/SKILL.md). Install upstream withnpx skills add davehague/skill-librarian --skill skill-librarian. Copyright stays with the author.
Skill Librarian
Migrate an agent skill into a central skill-library as a fully self-contained, portable unit — then prove it still works.
A skill is a folder of instructions (plus maybe some scripts) that an agent runtime loads. This came up around Claude Code skills in ~/.claude/skills/, which is the running example throughout — but the problem and the fixes are runtime-agnostic. Migrating skills out of Codex, Hermes, OpenClaw, or anything else works the same way: point the librarian at the skill's folder, and treat that runtime's entry/manifest file the way these instructions treat SKILL.md.
Why this exists
A skill that lives in ~/.claude/skills/<name>/SKILL.md often isn't really self-contained. It calls a script three directories away, reads an API token from a repo's .env, hardcodes /Users/you/... paths, or scribbles a running log into the repo it happened to live in. Copy that folder to another machine — or hand it to a friend, or load it into a different agent — and it breaks in ways that only show up at runtime.
This skill fixes that, one skill at a time. The output is a folder that someone else can clone, fill in one config.json, and run.
The bar to clear: after migration, the skill works from its new home with nothing outside its own folder except (a) other skills it explicitly depends on and (b) the values in config.json.
Configuration
The one input per run is the path to the source skill folder — the user points you at a folder; you don't hunt for it. config.json holds only the machine-level settings reused across every migration.
Read config.json from this skill's folder ($SKILL_DIR/config.json, where $SKILL_DIR is the directory containing this SKILL.md). If it's missing, copy config.example.json to config.json and ask the user to confirm the paths. See README.md.
| Key | Meaning |
|---|---|
skill_library_path |
Destination repo where migrated skills land, as an absolute path (e.g. /Users/<username>/source/skill-library) |
state_root |
Where relocated state goes, absolute (e.g. /Users/<username>/.local/state/skills) |
Path style — one rule, no exceptions
Every path in any config.json is a full, literal, absolute path, used verbatim with no expansion (never ~, never $HOME). config.json gets read by Python, bash, and argparse, and they expand ~/$HOME differently — argparse not at all — so a stored tilde is a latent bug. config.example.json shows paths as /Users/<username>/.... Tilde / $HOME / XDG expansion is fine only inside code (a tool's own state dir, a default output location), which is a single known context.
Notation
Two token styles appear below and mean different things: $NAME is a live shell variable the commands set and reuse ($SKILL_DIR, $FRAMEWORK, $HOME); <name> is a fill-in token — replace it with the real value (the skill's name, a config.json value, a sample arg) before running.
The workflow
Work one skill at a time. Create a TodoWrite list with the seven phases below so nothing gets skipped. The easiest steps to drop — and the most expensive to skip — are the gate after the audit (don't change files before the human signs off), the two verify phases (static checks, then the end-to-end run), and the clean-up phase.
Phase 0 Locate & guard → find the skill; bail if it already exists in the library
Phase 1 Audit dependencies → classify everything the skill touches
─ GATE Sign off with human → report findings + plan; get the OK before changing any files
Phase 2 Migrate → copy the body, vendor scripts, extract config, relocate state
Phase 3 Verify → grep-clean, RUN the scripts, confirm secrets are git-ignored
Phase 4 Prove it end-to-end → run the whole skill via a minimal-context subagent; fix what it flags
Phase 5 Report → tell the user what moved, what config they must fill in
Phase 6 Deploy & clean up → link the new skill into the runtimes, then tidy up with the user
Phase 0 — Locate & guard
- Take the source skill folder the user points you at. Read its entry file (
SKILL.md, or the runtime's equivalent) and settle on the skill's name — default to the folder name unless the manifest says otherwise. - Check
<skill_library_path>/<name>/. If it already exists, stop and report back — do not overwrite. Migrations are not idempotent (config.json may hold real secrets; the body may have local edits). Let the user decide how to reconcile. - Confirm the destination name with the user if it's ambiguous.
Phase 1 — Audit dependencies
Read the SKILL.md and list every file in the source folder. Then classify everything it touches. This audit drives the whole migration, so be thorough — grep the body and any scripts for path-like and secret-like strings.
| What you find | How to detect it | Action in Phase 2 |
|---|---|---|
| External script / project | references to scripts/..., ../, another repo path; a script the body calls |
Vendor it into <skill>/scripts/ (respect .gitignore — see below) |
| Shared rules/docs from a workspace (not a skill) | markdown/CSS/prompts the body reads that live in another repo or workspace | Vendor into <skill>/references/<source>/ (provenance-named subfolder) |
| Dependency on another skill | "use the X skill", Skill(...), /some-skill |
Reference only — never copy. Note it as a prerequisite in the skill's README. |
| User-provided secret | an API key/token/password the user supplies and pastes in | Extract to config.json |
| Tool-obtained credential | a token the tool fetches/refreshes itself (login, OAuth, device flow) and writes to disk | Relocate to <state_root>/<name>/ and point the tool there — it's state the tool manages, not user config |
| Hardcoded absolute path | /Users/..., a vault path, a fixed data dir |
Extract to config.json (absolute) |
| Output / export destination | where the skill writes the user's data or reports | Expose as a config.json key with a portable default; leave the content where the user expects it (don't move it into the state dir) |
| State the skill writes about itself | log, ledger, "running list", "last processed", a marker/.json it appends to |
Relocate to <state_root>/<name>/ |
Two judgment calls come up a lot:
- State vs. output: "if this file vanished, would the skill lose memory of what it's done?" Yes → state → relocate. Just a deliverable the user reads → output → leave the content, but make its destination a config key.
- Secret vs. credential: did the user hand you this secret (→
config.json), or did the tool obtain it through a login/OAuth flow and write it to disk (→state_root/<name>/)? The Snipd-stylelogintoken is the latter.
Phase 1b — Determinism audit (classify the workflow, not just the dependencies)
While you're reading the body anyway, classify each step of the skill's workflow:
| Class | Test | Examples |
|---|---|---|
| Mechanical | A script could do it byte-for-byte: fixed paths, fixed commands, selection by rule, file reads/writes, sending, logging | "run date", "pick the newest unprocessed item vs a log", "email the file", "append a TSV row" |
| Judgment | The step's value IS the model's judgment: writing in a voice, summarizing, deciding, adapting to messy input | "write the newsletter", "draft posts in David's voice", "triage the incident" |
Then report the ratio and, when it's lopsided, propose compiling the mechanical steps into scripts/ during migration: a runner does the plumbing, and each judgment step becomes ONE claude -p call with all tools disabled and its input on stdin. The full pattern, its payoff, and two worked examples live in references/determinism-audit.md — read it before proposing a compile.
When to push, when to leave alone:
- Push hard for skills that run unattended (a scheduled job invokes them headlessly). Every mechanical step an agent executes is a place it can stall on a prompt no one will answer — two real jobs burned 2h/night this way — and a toolless call is structurally immune. For these, the audit's compile proposal is the default, not an option.
- Leave interactive, exploratory skills as prose. If the workflow's value is adapting to what it finds (debugging, research, conversation), compiling removes the point. A 90%-judgment skill gets a "leave as prose" verdict — that is a finding, not a failure.
- Never compile the voice. Prose briefs stay in
SKILL.mdand the runner extracts them at runtime; duplicating prose into code is how the two drift.
Gate — sign off with the human (before touching files)
The audit produces a picture; don't start changing files until the human has seen it. This is the cheapest moment to catch a wrong call. Report back concisely, then wait for a go:
- What you found — the dependency classification (what gets vendored, what's referenced as a sibling skill, what becomes a
config.jsonkey, what's state vs. output), and especially the non-obvious judgment calls: a shared, read-only datastore → config, not a vendored copy; personal data (a bio, an audience profile) → config, kept out of the committed skill; a whole workspace's shared docs → vendored intoreferences/<source>/. - The plan — the config keys you'll create, the scripts you'll vendor and whether they need
uv, where state/output will land, what you'll rewrite in the body. - Determinism verdict (Phase 1b) — the mechanical/judgment ratio and your call: compile the mechanical steps into
scripts/(say which, and what the one-or-few toolless LLM calls will be), or leave as prose (say why). For a skill that runs unattended, compiling is the default recommendation. - Name — now that you understand what the skill actually does, assess its name. Always show the current name and always make a recommendation — even when you'd keep it. If the workflow points to something clearer or more accurate, propose it (e.g.
newsletter-setup→newsletter-profile-setup, since it builds a profile, not sending infrastructure); if the current name is already the best fit, recommend keeping it and say why. The user picks; the chosen name becomes the library folder and the deployed skill name — re-check it doesn't collide in the library if you changed it. - Open questions — only the genuinely ambiguous ones that would change the plan. Don't ask about what you can sensibly default; do ask when the right call depends on how the user works.
Then stop and let them confirm, adjust, or redirect — their answer can change what Phase 2 builds, which is the whole point of asking now instead of after. If the user has already told you to just proceed, state your plan in one pass and continue without waiting.
Phase 2 — Migrate
With the plan signed off at the gate above, build <skill_library_path>/<name>/.
a. Copy the body. Copy SKILL.md verbatim first; you'll edit the copy, never the original. The original keeps running until the user decides to switch over.
b. Vendor scripts — but respect the source's .gitignore. Before copying a script or project folder in, read the source repo's .gitignore (it's often at the repo root, not next to the script). Never carry across:
.venv/,venv/,__pycache__/,*.pyc,*.egg-info/node_modules/,dist/,build/,.next/,.turbo/.envand anything else the repo ignores
For a Python tool, vendor the source files only — never the virtualenv, and never depend on a globally pip installed package (that pollutes the machine's Python and collides across skills). Isolate dependencies with uv (the library default; a documented prerequisite):
- Single-file script → declare its deps inline with PEP 723 and run it via
uv run(uv builds a cached, isolated env per dependency-set; nothing touches global Python):#!/usr/bin/env python3 # /// script # requires-python = ">=3.10" # dependencies = ["pypdf"] # /// - Multi-file project → make a venv in the state dir and install into it:
uv venv <state_root>/<name>/.venv && uv pip install ..., then invoke scripts with that interpreter. The venv is machine-specific, so it lives with state, not in the committed folder.
For a Node tool, copy package.json (and lockfile), not node_modules. The goal is "reproducible and isolated on a fresh machine," not "byte-for-byte copy."
Where vendored things go — two standard folders, don't invent more: runnable code → <skill>/scripts/. Reference material the body reads (rules, prompts, CSS, docs) → <skill>/references/; when it's borrowed from another skill or workspace, use a provenance-named subfolder <skill>/references/<source>/ (e.g. references/content-to-guide/).
c. Rewrite the vendored scripts to be location-independent. A vendored script must resolve its own location and read config from the skill root, not from wherever it used to live:
# was: load_dotenv(Path('/Users/me/some-repo/.env'))
config = json.loads((Path(__file__).resolve().parent.parent / "config.json").read_text())
token = config.get("some_token") or os.getenv("SOME_TOKEN") # env fallback is a nice touch
Drop now-unneeded deps (e.g. python-dotenv) from the script's inline dependencies list.
d. Extract user-provided secrets & paths into config. For every user secret and hardcoded path from the audit:
- Add a key to
config.example.jsonwith a placeholder that shows the shape: paths as absolute/Users/<username>/..., secrets asyour_<thing>_here. - Add the real value to
config.json(git-ignored), as an absolute path (expand any~/$HOMEbefore writing). When the value already exists on this machine (a token in a repo.env, a known path), copy it in programmatically, without printing the secret to the terminal. - In
SKILL.md, replace the literal with aconfig.jsonread, used verbatim (no expansion). Establish the$SKILL_DIRconvention near the top so every command can find config:
Tool-obtained credentials are not config — they go to state, see (e).vault_path=$(python3 -c "import json; print(json.load(open('$SKILL_DIR/config.json'))['vault_path'])")
e. Relocate state and tool-obtained credentials. Point every state write — and any credential the tool fetches for itself (a login/OAuth token it writes to disk) — at <state_root>/<name>/. For a vendored tool that hardcodes something like ~/.foo_token, repoint its token path to the standard state location and create the dir on write:
_STATE = os.path.join(os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state")), "skills", "<name>")
os.makedirs(_STATE, exist_ok=True)
TOKEN_FILE = os.path.join(_STATE, "token")
Migrate existing token/state by copy if the original location is still used elsewhere (e.g. a scheduled job that points at it), otherwise move it.
f. Generate the support files (the portable-skill pattern):
config.example.json— committed, shows every key with placeholders (absolute/Users/<username>/...paths,your_*_heresecrets), no real valuesconfig.json— git-ignored, real values for this machine.gitignore— containsconfig.json(a repo-root.gitignorewith**/config.jsonalso works; add a local one to be safe)README.md— human setup: copy config, fill keys, noteuvas a prerequisite (deps are isolated viauv run/ a state-dir venv, never installed globally), list any prerequisite skills and MCP servers
A skill with no secrets, no hardcoded paths, no scripts, and no state is just SKILL.md — a one-file copy. Don't manufacture config files it doesn't need.
Phase 3 — Verify (do not skip)
A migration that looks moved but silently broke a path is worse than no migration — it fails when the user is depending on it. Run the same three checks every time, after the copy/edits are done:
-
Grep clean. No leftover source coupling in the migrated
SKILL.mdand scripts:grep -rnE "/Users/[a-z]+|\.env|load_dotenv|<source-repo-name>|os\.getenv\(['\"][A-Z_]+" <skill_library_path>/<name>Then classify each hit rather than reacting to the count. A literal
/Users/<you>/…the code actually uses is real coupling: go back to Phase 2. But a skill that is about paths, or written in someone's voice, legitimately contains example paths and names in its prose — those are illustrative, not coupling. What must be gone is functional coupling: real machine paths the code reads,.env/load_dotenv, the source repo name in a code path. Fine to leave:$SKILL_DIR,config.jsonreads,<state_root>, and in-code~//$HOMEdefaults. -
Run it — through the isolated env, not global Python. Execute every vendored script from the new location so it exercises both the dependency isolation and the
config.jsonread:SKILL_DIR=<skill_library_path>/<name>; uv run "$SKILL_DIR/scripts/<script>.py" <sample-args>Running with
uv runproves the deps resolve from the script's own declaration (or the skill's state-dir venv) — so "it works" never means "it works because a package happens to be installed globally." (Quick sanity check:uv run python -c "import <dep>"without the script should fail — confirming nothing leaks from the ambient environment.) If a script can't fully run here (needs interactive auth, a device, live data), at minimum import-check it throughuv runand confirm it finds and parsesconfig.json. Note in the report what was fully run vs. smoke-tested. Read the raw output — a warning on stderr is fine, a traceback or empty stdout is not. -
Secret-safety. Confirm git ignores the secret and tracks the example:
git -C <skill_library_path> check-ignore <name>/config.json # must print the path git -C <skill_library_path> status --short <name>/ # config.example.json staged, config.json absent
If any check fails, fix it before moving on. Don't report success on an unverified migration.
Phase 4 — Prove it end-to-end (minimal-context subagent)
Phase 3 confirms the parts; this confirms the whole — that a fresh agent handed only the skill can actually do the job on a real input. It catches what the static checks can't: a vendored doc still pointing at the old path, a step that silently relied on ambient context, a config key the body forgot to read, a dependency that only resolved because something happened to be installed globally. If the migration has a hole, this is where it shows — and it's the easiest phase to skip, so don't.
- Minimal context is the point. Dispatch a subagent (the runtime's task/subagent tool) and give it almost nothing: the absolute path to the migrated
SKILL.md, the value of$SKILL_DIR(so itsconfig.jsonreads resolve), one realistic task/input, and the instruction to follow thatSKILL.mdby path — and explicitly NOT to use any installed skill of the same name (the original is often still loaded). Don't feed it the conventions, the audit, or hints. If it needs something you didn't put in the skill, that's a finding. - Real but bounded input. Representative and small (a few items, not hundreds) so the run is fast and the output is easy to check.
- Gate outward side-effects. If the skill's happy path publishes, emails, pushes, or otherwise acts off the machine, confirm scope with the user first (full end-to-end vs. stop before the outward step). A local-output-only skill can run freely.
- Have it report friction precisely. The subagent is a fresh pair of eyes — ask it to quote anything in the
SKILL.mdthat was unclear, broken, missing, or worked-around. Apply those fixes now, while concrete; some turn out to be improvements to the original skill, not just the migration. - A subagent usually can't spawn its own subagents. If the skill's workflow fans out to subagents, the tester will run that step inline instead — a runtime limit, not a skill defect. Note it, and consider giving the skill a sequential fallback.
If a full run genuinely can't happen here (interactive auth, a device, live credentials it shouldn't use), say so in the report and fall back to the Phase 3 component run — don't silently skip this.
Phase 5 — Report
Tell the user, concisely:
- Moved/vendored: which scripts came across, which were dropped (
.venv, etc.) - Config keys created: and which you auto-filled vs. which they must fill in
- State relocated: old path →
<state_root>/<name>/ - Prerequisite skills/MCPs: anything referenced but not copied
- Verification: static checks (Phase 3) + the end-to-end run (Phase 4) — what passed, what the test subagent flagged and you fixed, what was only smoke-tested
- Not committed unless they asked — leave staging to them
Phase 6 — Deploy, then clean up
The migration is verified and reported. Now deploy the skill so it actually loads, then tidy up. Deploying is something you do (safely — see below); the cleanup items are offered, and nothing that touches files the user already had goes ahead without a yes.
- Deploy it — do this yourself, don't just hand over a command. Deploying links the skill into every runtime's skills dir (symlink on macOS/Linux, directory junction on Windows) so it loads now, and future edits /
git pulls propagate everywhere.deploy.pylives at the root of the skill-librarian framework repo, one level up from this skill's own folder. Run it:- Locate
deploy.pyby resolving this skill's real path (it's usually symlinked into the runtime dir, so follow the link):FRAMEWORK=$(python3 -c "import os,sys; print(os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[1]))))" "$SKILL_DIR/SKILL.md") - Register the skill's library.
deploy.pyonly sees skills in the libraries listed underlibrariesin$FRAMEWORK/deploy.json. The skill you just migrated lives inskill_library_path; if that path isn't already indeploy.json'slibraries, add it (or ask the user to), or--skillwon't find it. - Preview, then link:
python3 "$FRAMEWORK/deploy.py" --dry-run --skill <name> # show the plan python3 "$FRAMEWORK/deploy.py" --skill <name> # do itdeploy.pyprompts before replacing any real (drifted) directory, so running it is safe; nothing destructive happens without a yes.
- Locate
- The original skill. Once deployed, the source copy is redundant and will double-load if it sits in a dir the same runtime also reads. Lay out the options:
- Delete it (recommended once deploy succeeds) — the verified library copy now loads via the link, so the original is a stale duplicate. Confirm before deleting.
- Leave it — only if it's in a dir the runtime doesn't also load; otherwise it collides with the deployed link.
- Migration scratch. The verify run may have left
__pycache__/or*.pycin the vendored scripts, or temp/test output. Offer to remove it. (git -C <skill_library_path> status --short <name>/shows anything stray.) - Now-orphaned source scripts you vendored in (e.g. an external
scripts/foo.py). They may be removable from the source repo, but only if nothing else uses them — the user knows that, you don't. Point them out. - Old state files at the pre-migration location, if you relocated state and didn't move the history across.
Conventions this skill enforces
$SKILL_DIR= the folder holdingSKILL.md. The running agent resolves it; config is always$SKILL_DIR/config.json.- Two standard folders:
scripts/for runnable code,references/for docs the skill reads on demand (borrowed material inreferences/<source>/). Don't invent new top-level folders. - Dependencies are isolated with
uv— PEP 723 inline deps +uv runfor single-file scripts, a state-dir venv for projects. Neverpip installinto the global/system Python. - Scripts self-locate via
Path(__file__)and read config from the skill root — they never assume a working directory. - State lives under
<state_root>/<name>/, never inside the skill folder (keeps the folder a clean, shareable artifact) and never inside a random repo. - Secrets never appear in committed files or terminal output.
Golden reference
references/case-study-daily-summary.md walks through one full migration end to end — a skill with a vendored script, an extracted secret, and an extracted path — showing how the phases play out and what the finished folder looks like. Read it when you want a worked example to match. references/migration-recipes.md has the atomic before/after snippets for the common cases (dotenv→config, hardcoded path→config, state relocation, vendoring a Python CLI). references/determinism-audit.md is the Phase 1b companion: the compile pattern (mechanical steps → scripts/, judgment steps → toolless claude -p calls), the failure modes it eliminates, and two worked examples.
Dogfooding
This skill follows its own pattern: its machine-specific paths live in config.json, and it carries a config.example.json, .gitignore, and README.md. It belongs in the library too — migrating it is a good self-test.