Cross-runtime AI agent skill housekeeping: spot duplicates, drift, broken symlinks, junk, and stale skills across Claude Code, Codex, Cursor, OpenClaw, and more.
Project description
30x Skill Doctor
Spring-cleaning for your AI agent skill library.
If you use Claude Code, Codex, Cursor, OpenClaw, or any combination of them,
your ~/.claude/skills/, ~/.codex/skills/, ~/.cursor/skills/, and plugin
directories are probably a graveyard. The same skill installed three times under
three different names. Old version in one runtime, newer version in another.
Symlinks pointing at deleted files.
30x-skill-doctor scans the lot, surfaces what's wrong, and walks you through
a fix โ interactively, with a backup, and a one-line undo.
Three steps
1. Install
pipx install skill-doctor
Or with uv:
uv tool install skill-doctor
Plain pip works too: pip install skill-doctor.
Optional: install
asm(npm install -g agent-skill-manager) to unlock the SKILL.md write-quality dimension. Everything else works without it.
Want the Agent to call it for you?
Run once:
skill-doctor install-skill
This registers skill-doctor as a Claude Code / Codex / Cursor / OpenClaw
skill. After that, when you say things like "audit my skills" or "do I
have duplicate skills" in any Agent chat, the Agent will invoke
skill-doctor automatically โ no need to remember the CLI.
2. See what you have
skill-doctor
You'll get a one-screen report like this:
๐ฉบ Health: 73/100 C+
๐ You have 453 skills across 8 runtimes:
Claude Code 154
OpenClaw 151
Plugin (Claude) 48
Agents 44
Codex 31
Plugin (Codex) 14
OpenCode 9
Cursor 2
Categories:
Other (147) | SEO (91) | Marketing (59) | Dev (47) | Ads (41) | ...
๐ 70 duplicate groups (187 instances)
๐ก 38 drift conflicts (same name, different content)
โ 25 broken symlinks
๐ SKILL.md write quality
B:23 C:262 D:150 F:18
โ Clean up: skill-doctor clean
Dimensions with zero findings are hidden automatically. Clean machines just see the inventory and a green checkmark.
3. Tidy up
skill-doctor clean
It walks through every issue interactively:
[1/48] Merge ads-google (claude)
~/.claude/skills/ads-google โ symlink to ~/.openclaw/skills/ads-google
Apply? [y/N/q/a (a = yes-to-all-of-this-type)]
- y โ apply this one
- N โ skip (default; bare Enter also skips)
- q โ stop right here
- a โ yes-to-all of this action type (no more prompts for it)
Anything destructive is mv'd to ~/.skill-doctor/backup/<timestamp>/ first,
not removed. Roll back the last apply with:
skill-doctor undo
Need to recover an older one? skill-doctor undo --pick.
Share your library's health
skill-doctor share
Generates a self-contained SVG you can drop into Twitter / Reddit / GitHub Issues, plus an ASCII version for screenshots and a Markdown snippet (auto-copied to your clipboard for chat windows).
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ ๐ฉบ SKILL LIBRARY HEALTH โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 87 / 100 Grade: B+ โ
โ 453 skills ยท 8 runtimes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Top runtimes: โ
โ Claude Code 154 โ
โ OpenClaw 151 โ
โ Plugin (Claude) 48 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Findings: โ
โ duplicates ๐ 5 โ
โ drift ๐ก 3 โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
The health score is a transparent 0โ100 subtractive model โ every penalty
is listed in src/skill_doctor/health.py
and the score appears in the --json output too if you want to track it
over time in CI.
Seven dimensions it checks
| What it catches | What clean does about it |
|
|---|---|---|
| ๐ Categories | Auto-tags every skill (SEO / Ads / Marketing / Dev / โฆ) | โ |
| ๐ Duplicates | Identical SKILL.md (sha256) under multiple runtimes | Replace copies with symlinks to a smart-elected master |
| ๐ก Drift | Same name, different content (e.g. v1.1 in Claude, v1.0 in OpenClaw) | Surface only โ you choose the source of truth |
| โ Broken | Symlink whose target doesn't exist anymore | Remove the dead link |
| ๐ Junk | macOS * 2.md, .DS_Store, vim swap files, etc., anywhere in the tree |
Backup and delete |
| ๐ฐ Stale | Skill directory untouched for > 90 days (mtime) | Just flagged โ your call |
| ๐ Write quality | SKILL.md hygiene + suggested fixes (cached, near-instant after first scan) | โ |
Master election (the dedup heart)
When several copies of the same skill exist, one becomes the canonical source the others symlink to. The election score is transparent:
score = version (40%) + inbound symlinks (40%) + mtime (20%)
When no instance in a duplicate group declares a metadata.version, the
version weight is redistributed evenly to the remaining two signals โ the
algorithm doesn't pretend a 40% lever is doing work it isn't. Weights are
tunable in ~/.skill-doctor/config.toml. Path-depth weight (path_depth)
is recognised but defaults to 0.0 โ it was dropped in v0.4.0 because no
community convention supports "deeper path = more canonical."
Methodology
Each diagnostic dimension is documented as a four-part contract: definition (what is being asserted), detection (the algorithm that produces the assertion), provenance (the standards or specifications the algorithm relies on), and failure modes (where the assertion can be wrong, and the direction of error). Objective facts derived from formal specifications are kept separate from heuristics calibrated by the maintainer.
๐ Categories
Definition. Every discovered skill receives a single label from a closed
set: SEO, Marketing, Dev, Ads, Deploy, Data, Design, AI/Video,
Other, Uncategorized.
Detection. Two-pass classifier evaluated in priority order:
- Path-prefix match against a hand-curated lookup table (
30x-seo-*โ SEO,ads-*โ Ads, etc.). - Description-keyword match against a per-category bag of substrings.
- Fallback to
Otherif at least one keyword fires;Uncategorizedif the skill has no description.
Provenance. No external specification governs categorization. The lookup
table lives in src/skill_doctor/classify.py and is the canonical authority.
This dimension is a viewer convenience, not a correctness check.
Failure modes. Skills with bespoke naming and no domain keywords land in
Other. On a 453-skill library, ~32% currently fall into Other; this is a
classifier-coverage limit, not a user error.
๐ Duplicates
Definition. A duplicate group is a set of two or more SKILL.md files with byte-identical normalized bodies AND identical directory basenames.
Detection.
key = ( sha256( normalize(body) ), basename(skill_dir) )
dup = { key | |instances(key)| โฅ 2 }
Body normalization strips trailing whitespace per line and collapses leading and trailing blank lines. Identical basenames are required to prevent unrelated skills with coincidentally-identical content from being merged.
Master election. For each duplicate group, exactly one instance is elected as the canonical source by a weighted score across four signals:
| Signal | Weight | Rationale |
|---|---|---|
metadata.version |
40% | Higher declared SemVer is the strongest signal of "newest writeable copy". When no instance in the group has a version, this 40% redistributes equally to the other two axes. |
| Inbound symlinks | 40% | If N sibling instances already resolve to this path, it is in fact the source. Strongest empirical signal in practice. |
mtime recency |
20% | Tiebreaker for instances with otherwise equal provenance. |
All weights are configurable via ~/.skill-doctor/config.toml. A
path_depth weight is recognised by the loader but defaults to 0.0;
earlier versions of skill-doctor used a 15% path-depth weight on the
intuition that "deeper paths are monorepo sources," but a community survey
found no published convention to support this โ the inverse intuition
(root-level = canonical) is implied by Claude Code's enterprise > personal >
project precedence โ so the weight was removed in v0.4.0. Set it back if
your project has a clear deeper-is-source convention.
Designating a master manually
For projects with a clear canonical source convention (skillshare-style), override the elected master per run:
skill-doctor clean --master openclaw # OpenClaw wins every duplicate group
Provenance. SHA-256 is specified in NIST FIPS PUB 180-4 (August 2015). The content-addressable equivalence model follows Git's object model (Chacon & Straub, Pro Git, 2nd ed., ch. 10).
Failure modes. Detection is exact: SHA-256 collision probability is bounded at โ 2โปยฒโตโถ and is treated as zero. Master election, by contrast, is a heuristic and may diverge from a user's project-specific source-of-truth convention; manual override is supported by editing the resulting plan before applying.
๐ก Drift
Definition. Two or more instances share a directory basename but differ in SHA-256 hash โ i.e., the same skill identity has divergent content across runtimes.
Detection. Group by basename; emit a drift group whenever the set of
distinct hashes inside that group has cardinality โฅ 2.
Policy. Drift is never auto-resolved. Divergence is frequently intentional โ for example, when a user customizes a skill for a specific runtime โ and silent merging risks data loss. The tool surfaces drift; the user designates the source of truth.
Provenance. Detection follows the same content-addressable model as duplicate detection. The non-resolution policy is a maintainer-chosen safety invariant, not derived from an external spec.
Failure modes. A single-byte difference (e.g., a trailing space) is sufficient to register drift. False-positive risk is non-trivial; this is a deliberate trade against the larger risk of silent data loss.
โ Broken symlinks
Definition. A path that is itself a symlink, but whose target does not resolve to an existing filesystem entry.
Detection. path.is_symlink() and not path.exists(). Equivalent to
POSIX find <root> -xtype l.
Policy. clean calls unlink(path). Symlinks store no payload, so
removal is loss-free.
Provenance. POSIX.1-2017 (IEEE Std 1003.1โข-2017) ยง4.1.7 (symbolic link
resolution) and ยงlstat. Confirmed by macOS lstat(2).
Failure modes. None at the detection layer. The relation is exact under POSIX semantics.
๐ Junk files
Definition. Files inside a skill tree that are not user content but rather artefacts of the host filesystem, sync layer, or editor.
Detection. Filename matches against a curated regex catalog:
| Pattern | Source |
|---|---|
* \d+\.(md|json|py|...) |
iCloud Drive filename-collision suffixing |
.DS_Store |
macOS Finder metadata |
._* (AppleDouble) |
macOS extended-attribute side-files |
[._]*.s[a-v][a-z], [._]*.sw[a-p] |
Vim swap, per github/gitignore Global/Vim.gitignore (canonical, 173k stars) |
[._]s[a-rt-v][a-z], [._]ss[a-gi-z] |
Vim swap (root-level), same source |
[._]*.un~, *~ |
Vim persistent undo / Emacs backup |
__MACOSX/ |
macOS-injected zip metadata directory |
Excluded outright (defense-in-depth against
anthropics/claude-code#32637,
where another tool destroyed user data via cp -a + rm -rf on 0-byte
iCloud-offloaded stubs):
- File suffix
.icloud - Any path under
~/Library/CloudStorage/or~/Library/Mobile Documents/
Scan is recursive across the entire skill directory tree (Path.rglob).
Provenance. Each entry corresponds to a documented behavior of the producing system. The set is curated against observed pollution on real machines, not generated from a single spec.
Failure modes. Deliberately-named files matching the patterns (e.g.,
top-10 2.md) will be flagged. Mitigated by mandatory pre-deletion backup
under ~/.skill-doctor/backup/<timestamp>/ and one-command undo.
๐ฐ Stale
Definition. A skill whose tree has not received any modification within a configurable time window โ flagged for review, not declared obsolete.
Detection. max(stat.st_mtime for f in tree) compared against
now - threshold. Default threshold is 90 days; override with
--stale-days N.
Provenance. POSIX stat(2) modification time. The 90-day default is a
maintainer-chosen heuristic, not specification-derived.
Failure modes. This is the weakest dimension epistemologically. Modification time is not invocation time; the tool has no access to runtime telemetry. A skill that is stable, correct, and invoked daily will be flagged stale if its files have not been edited in 90 days. The output should be treated as a "consider reviewing" prompt, never as a deletion recommendation.
๐ Write quality
Definition. A per-skill score (B/C/D/F) and ordered list of concrete rewrite suggestions, evaluating SKILL.md hygiene against the canonical agent-skill authoring specification.
Detection. Aligned with Anthropic's official skill-creator
specification โ the source of truth for what constitutes a well-formed
SKILL.md. Results are cached per file by mtime; first run on a 453-skill
library takes โ 40 s, subsequent runs โ 0.4 s with no edits.
Provenance. Anthropic skill-creator skill defines the rule set (frontmatter required fields, body length, description format, naming conventions, progressive-disclosure structure).
Failure modes. Quality scoring requires the underlying evaluator to be
available on PATH. When asm is not installed, the dimension renders a
visible locked placeholder with the install command โ
npm install -g agent-skill-manager โ rather than disappearing silently;
the other six dimensions function unchanged.
AI handoff for the human-judgment dimensions
Three of the seven dimensions โ drift, stale, and quality โ
require a judgment Skill Doctor refuses to make automatically: drift could
be deliberate per-runtime tuning, stale could be stable rather than dead,
quality fixes are skill-specific authoring decisions. Instead of forcing
you to read Nร2 SKILL.md files yourself, clean offers to generate an
AI handoff prompt for each dimension that has unresolved items.
$ skill-doctor clean
[ ...auto-fix dedup / broken / junk... ]
๐ก 38 drift groups remain โ Skill Doctor doesn't auto-resolve these.
Triage drift with AI? [y/N] y
โ Prompt copied to clipboard.
Paste into Claude.ai / Cursor โ follow the AI's table.
(Backup at ~/.skill-doctor/handoff/drift-20260506-145701.md)
๐ฐ 3 stale skills remain โ could be obsolete or just stable.
Triage stale with AI? [y/N] y
โ Prompt copied to clipboard.
๐ Low-grade SKILL.md files exist โ asm gave generic fixes; AI can
translate them into concrete edits.
Triage quality fixes with AI? [y/N] n Skipped.
What's in the prompt. Each handoff is a self-contained markdown file designed for the AI to consume, not for you to read:
| Handoff | Embeds | Asks AI to return |
|---|---|---|
| drift | unified diff (capped at 50 lines per pair) | merge_to_<runtime> / keep_divergent / needs_more_context + exact cp -R command |
| stale | frontmatter + first 60 lines of SKILL.md | likely_obsolete / still_useful / uncertain + rm -rf command (only if high-confidence obsolete) |
| quality | asm's flagged issues + first 60 lines of SKILL.md | A 3-bullet punch list of skill-specific edits โ you apply by hand |
Total prompt size for a typical 38-drift / 3-stale / 10-quality run is ~20 KB combined, well within Claude.ai or Cursor's working context.
Workflow. Paste prompt โ AI returns a recommendation table โ you copy
the commands you accept and run them in your terminal โ rerun
skill-doctor clean to verify the count drops.
Skill Doctor never edits these files itself. The handoff is a translation layer: it converts "you have 38 things to manually compare" into "you have a 38-row table to skim", but every actual file change remains a deliberate human action. This is consistent with the "not a sync engine" non-goal below.
The handoff prompt is auto-copied to the system clipboard
(pbcopy / wl-copy / xclip / xsel / clip) and saved to
~/.skill-doctor/handoff/<dimension>-<timestamp>.md as backup.
Why directory-level symlinks
When clean resolves a duplicate group, it keeps one master and replaces
the rest with directory-level symlinks โ i.e. ~/.codex/skills/foo
becomes a symlink to ~/.openclaw/skills/foo as a whole, never to
SKILL.md inside it.
This is a deliberate choice, not a coincidence. Three properties:
-
Drift cannot accumulate. One source of truth, N pointers. Editing the master is instantly visible to every runtime. A copy-mode equivalent would require a sync engine, which Skill Doctor explicitly is not (see Out of scope below).
-
It matches every comparable tool's default. skillshare, skills-hub, skills-supply, and the SSW skill-sharing rule all default to symlinks; copy is a fallback for the runtimes that refuse them.
-
It sidesteps the Codex bugs OpenAI marked not-planned. #15756 (with #17344 as duplicate) closed file-level SKILL.md symlink support; #11314 closed the case where
~/.codex/skillsitself is a symlink. Both are permanently unresolved. By symlinking at the directory level โ at~/.codex/skills/<skill>/, neither file-level nor the skills root โ Skill Doctor lands in the slice Codex CLI still handles.
Known edges
- Cursor: Cursor is observed to not reliably follow directory symlinks
for skills. skills-hub's README explicitly documents forcing copy on
Cursor targets for this reason. When
cleanwould symlink into a Cursor path, it prints a warning and suggestscp -Rfrom the master, or--exclude cursorto skip that target. The default behaviour does not change for the other six runtimes. - Whole-home sync across machines: If you sync
~/across machines and the symlink target lives outside that sync set, the link will be dead on the receiving machine. Either keep the master inside the synced tree, or use a per-runtime copy strategy outside Skill Doctor.
Out of scope
Skill Doctor explicitly does not:
- Act as a sync engine. It de-duplicates by symlink, not by maintaining N independent copies in lockstep. If you need true per-runtime copies with scheduled sync, use skillshare or similar.
- Fan a skill out to runtimes that don't have it. If a skill exists
only under
~/.codex/skills/and you want it to also appear under~/.claude/skills/, that's a one-lineln -syou do by hand. Skill Doctor will pick the new symlink up on the next scan and treat it as a resolved duplicate. We deliberately don't ship a fan-out command โ every variant (which runtimes, copy or link, propagate updates or not) is a policy decision that belongs to the user, not the tool. - Measure invocation frequency. Runtimes do not expose skill-level telemetry; modification time is not a substitute.
- Score overall "skill quality" beyond the SKILL.md hygiene captured above. Behavioral evaluation belongs to runtime-specific tooling.
- Detect malicious or adversarial patterns. For supply-chain or prompt- injection concerns, use a dedicated security auditor.
- Modify your filesystem without consent. Every state-changing operation
is interactive (
clean), backed up to~/.skill-doctor/backup/<timestamp>/, and reversible viaskill-doctor undo.
Common flags
skill-doctor # default report (with health score)
skill-doctor --full # one row per skill
skill-doctor --full --no-truncate # don't shorten long paths
skill-doctor --version # version info
skill-doctor --runtime claude # filter by runtime
skill-doctor --category seo # filter by category
skill-doctor --json # machine-readable
skill-doctor --stale-days 90 # change stale threshold
skill-doctor --quality-n 10 # quick uncached quality sample
skill-doctor share # write shareable SVG + ASCII + Markdown card
skill-doctor share --no-print # don't echo the ASCII version
skill-doctor install-skill # register as an Agent-invocable skill
skill-doctor install-skill --all # install into every supported runtime
skill-doctor install-skill --force # overwrite an existing SKILL.md
skill-doctor clean # interactive tidy
skill-doctor clean --yes # non-interactive (3-second cancel)
skill-doctor undo # roll back last apply
skill-doctor undo --pick # pick a past backup
Custom runtime paths
Have skills somewhere unusual? Drop them into ~/.skill-doctor/config.toml:
[[extra_runtimes]]
path = "~/my-skills"
runtime = "unknown" # or any known tag: claude / codex / openclaw / agents / ...
glob = "*"
[weights]
version = 0.40
incoming_links = 0.30
path_depth = 0.15
mtime_freshness = 0.15
FAQ
Will it auto-delete anything?
No. Everything destructive is mv'd to ~/.skill-doctor/backup/ first. Run
skill-doctor undo and the last apply is fully reversible.
Why no auto-fix for "drift"? Two divergent copies might be intentional (you tweaked one for a specific runtime). The tool refuses to guess; it only flags.
First run takes ~40 seconds โ what's it doing? Scoring every SKILL.md for write quality. Subsequent runs hit a per-file mtime cache and finish in < 1 second. Edit one SKILL.md and only that one is re-scored.
My skill folder isn't on the default list.
Add it under [[extra_runtimes]] in ~/.skill-doctor/config.toml.
Where does the write-quality score come from?
From asm (an open-source evaluator aligned with the Anthropic skill-creator
spec). If asm isn't installed, the column is simply omitted โ the other six
dimensions don't depend on it.
Developer quick-start
git clone https://github.com/norahe0304-art/30x-skill-doctor.git
cd 30x-skill-doctor
uv sync
uv run --no-editable pytest # 32 tests
uv run --no-editable ruff check .
uv run --no-editable skill-doctor # try it on your machine
Project layout in AGENTS.md (project doctrine) and
src/skill_doctor/AGENTS.md (module map).
License: MIT.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file skill_doctor-0.5.0.tar.gz.
File metadata
- Download URL: skill_doctor-0.5.0.tar.gz
- Upload date:
- Size: 93.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2011b5fed24affab42273169d48cd8c1ad22c59da38e8c3cb08c199a87b55ed6
|
|
| MD5 |
50990e7201c41813f1e7a142b3a3c082
|
|
| BLAKE2b-256 |
491e24b976fc68e75c51047ecce2c4794b34eb9ebd0bb46feb6f0701ee1b0978
|
File details
Details for the file skill_doctor-0.5.0-py3-none-any.whl.
File metadata
- Download URL: skill_doctor-0.5.0-py3-none-any.whl
- Upload date:
- Size: 68.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a5faccd28f94cae207233ab7361cea1e60aea47158f29cc126b8dd52b444cd6
|
|
| MD5 |
340604f764aa305a2162aa0494ce2616
|
|
| BLAKE2b-256 |
e209cb82f300512df4b9153af3a5cc4a9b007bd1c7a1f0a1428c60c71c1a2d9f
|