Skip to main content

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.

Demo


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.

2. See what you have

skill-doctor

You'll get a one-screen report like this:

๐Ÿ“‚ 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.


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:

  1. Path-prefix match against a hand-curated lookup table (30x-seo-* โ†’ SEO, ads-* โ†’ Ads, etc.).
  2. Description-keyword match against a per-category bag of substrings.
  3. Fallback to Other if at least one keyword fires; Uncategorized if 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:

  1. 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).

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

  3. 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/skills itself 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 clean would symlink into a Cursor path, it prints a warning and suggests cp -R from the master, or --exclude cursor to 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-line ln -s you 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 via skill-doctor undo.

Common flags

skill-doctor                       # default report
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 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

skill_doctor-0.4.4.tar.gz (78.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

skill_doctor-0.4.4-py3-none-any.whl (54.0 kB view details)

Uploaded Python 3

File details

Details for the file skill_doctor-0.4.4.tar.gz.

File metadata

  • Download URL: skill_doctor-0.4.4.tar.gz
  • Upload date:
  • Size: 78.3 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

Hashes for skill_doctor-0.4.4.tar.gz
Algorithm Hash digest
SHA256 2ba69ddb479999f34eca797497e33453a41b5439446fd31502ecc7531fd0a10b
MD5 fbf1f28dc55f27d15f633a9392104445
BLAKE2b-256 2855689ac2115ac9e0cfbb78e769740f6b8a6991d818b928900e9f5cc9733e12

See more details on using hashes here.

File details

Details for the file skill_doctor-0.4.4-py3-none-any.whl.

File metadata

  • Download URL: skill_doctor-0.4.4-py3-none-any.whl
  • Upload date:
  • Size: 54.0 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

Hashes for skill_doctor-0.4.4-py3-none-any.whl
Algorithm Hash digest
SHA256 de7f6e611f8ab9415d6d890142b98a8edb336d5820195afddd1c233721d14bd9
MD5 75f51b5358f63029618d0fde57bba867
BLAKE2b-256 a6a10d0677cf053528caa25f918e69c7e7ddc8b3f20245acbfcf6d1d0bd5a154

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page