omp-skilld
v1.4.0
Published
An unofficial OMP plugin that refreshes skills from GitHub repositories in the background
Maintainers
Readme
omp-skilld
An unofficial OMP plugin that keeps skills from GitHub repositories up to date, in the background.
gh skill install --all takes upwards of a minute, and OMP loads extensions before it scans for skills — so refreshing on the critical path would put that minute on every single launch.
Skilld fires the refresh off unawaited and lets the next launch pick up whatever landed, pinning what it is doing under the editor — and how it turned out — so a first run does not just sit there looking broken.
Downloads land in a directory of the plugin's own and are published into the one OMP already scans, so nothing has to be configured for a skill to show up.
[!NOTE] Not built by the OMP team, and not affiliated with them in any way. Ported from opencode-skilld, which discovers skills differently, so the paths are OMP's own here. A machine running both tools downloads each source twice unless
targetandstampare pointed at the opencode ones.
Requirements
ghonPATH, logged in, recent enough to havegh skill— which is itself in preview and "subject to change without notice", so an olderghwill not have it at all.
A missing or unauthenticated gh is not fatal: you get an error toast and whatever skills you already had.
The skill API's rate limit is tight, and --all spends it per source, so the plugin is built to download each set of skills once rather than once per launch: a download that outlives its launch is finished and installed by the next one, and a failure is left alone for an hour instead of retried every time OMP starts.
Install
From a local checkout, which needs nothing published or pushed:
omp plugin link /path/to/omp-skilldA symlink rather than a copy, so edits are live on the next launch.
install also takes git specs — github:user/repo[#ref], gitlab:, bitbucket:, codeberg:, sourcehut:, and full git URLs — so a pushed repository can be installed without publishing to npm:
omp plugin install github:Attacktive/omp-skilldAnd once it is on npm, by name:
omp plugin install omp-skilldAll three land in ~/.omp/plugins/node_modules/omp-skilld and are recorded in ~/.omp/plugins/omp-plugins.lock.json.
omp plugin list shows it, omp plugin doctor checks it over, and omp plugin uninstall omp-skilld takes it out again.
Nothing refreshes until sources is set, so installing on its own is inert.
Configure
Settings are stored in ~/.omp/plugins/omp-plugins.lock.json and managed through the CLI:
omp plugin config set omp-skilld sources 'anthropics/skills'
omp plugin config list omp-skilldsources takes a plain list, separated by commas or whitespace, and JSON for the sources that need more than a name:
omp plugin config set omp-skilld sources 'anthropics/skills, someone/their-skills'
omp plugin config set omp-skilld sources '[{"repo": "anthropics/skills", "target": "~/skills/anthropic"}]'
omp plugin config set omp-skilld sources '[{"repo": "anthropics/skills", "pin": "v2.3.0"}]'
omp plugin config set omp-skilld sources '[{"repo": "anthropics/skills", "interval": 604800000}, {"repo": "obra/superpowers", "interval": false}]'
omp plugin config set omp-skilld sources '[{"repo": "anthropics/skills", "skills": ["frontend-design", "skills/engineering/reviewer"]}]'
omp plugin config set omp-skilld sources '[{"repo": "anthropics/skills", "include": ["frontend-design", "mcp-builder"]}, "obra/superpowers"]'
omp plugin config set omp-skilld interval 604800000A project can override any of it for itself in .omp/plugin-overrides.json, which OMP looks for in the working directory — there and nowhere else, so the override that applies is always the one omp plugin config list would show. The plugin asks OMP for its settings rather than resolving them itself, so the lock is found wherever OMP keeps it — the XDG layout included — and a plugin-overrides.json that will not parse is skipped exactly the way OMP skips it:
{
"settings": {
"omp-skilld": {
"sources": "someone/their-skills"
}
}
}That is the whole setup: OMP scans ~/.omp/agent/skills on its own, and the plugin publishes each selected skill from a download there as a symlink into ~/.omp/skilld/<slug>, one link per selected skill; include and exclude below control that selection.
Nothing has to be added to config.yml, and the links survive a refresh untouched — they point at a path inside the download, and a refresh only changes what that path holds.
A name you already hold there wins: a real directory of your own is never replaced, and the download keeps refreshing on disk in case you free the name later. Publication runs on every launch rather than only after a download, so a name you give up — or a link you delete by hand — is repaired on the next launch.
~/.omp/skilld/anthropics-skills/pdf/SKILL.md the download
~/.omp/agent/skills/pdf -> ~/.omp/skilld/anthropics-skills/pdf what OMP scansTo share one download with opencode-skilld, spell out the paths it uses:
omp plugin config set omp-skilld sources '[{"repo": "anthropics/skills", "target": "~/.local/share/opencode/skills/anthropic", "stamp": "~/.local/state/opencode/anthropic-skills-refreshed"}]'With no sources, the plugin does nothing at all.
Command
Skilld registers a small control surface inside OMP:
/skilld
/skilld status
/skilld refresh
/skilld refresh anthropics/skills/skilld and /skilld status report each configured source as fresh, stale, refreshing, waiting to install, failed, or never refreshed, together with its effective refresh policy and how many of that source's skills are actually published into OMP's skills directory.
A retained download complaint is shown beside a failed source when gh left one.
/skilld refresh forces every configured source past the normal freshness interval, and /skilld refresh <repository-or-label> targets one source by its exact repository or configured label.
The command still respects an in-flight download and the existing failure cooldown.
The refresh remains fire-and-forget: the command returns immediately, the usual pin/toast reports progress, and a download that outlives OMP is picked up on the next launch exactly like an automatic refresh.
Options
| Option | Default | Meaning |
|------------|-------------------|----------------------------------------------------------------------------------------------------------------|
| sources | [] | Repositories to refresh from: a list of owner/repo, or JSON for the object form below. |
| interval | 86400000 (24 h) | Default time a refresh stays fresh, in milliseconds. 0 refreshes every launch, which the rate limit will notice. |
A source given as an object can override what the bare owner/repo derives:
| Field | Default | Meaning |
|---------------|----------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| repo | — | The GitHub "owner/repo" to install from. Required, and refused unless it looks like one. |
| interval | global interval | Refresh interval for this source in milliseconds, or false for manual-only refreshes. |
| pin | latest upstream | Release, ref, or commit passed to gh skill install --pin. Must be a non-empty string when configured. |
| skills | all skills | Exact skill names or repository-relative skill paths to install. When absent, skilld keeps using --all. |
| target | ~/.omp/skilld/<slug> | Where to install. <slug> is repo with / turned into -. |
| stamp | ~/.omp/skilld/.<slug>-refreshed | Where the last successful refresh is recorded. |
| label | repo | The name used in toasts. |
| placeholder | "template" | The placeholder skill directory to drop from each download, or false to keep whatever upstream ships. |
| include | all downloaded skills | Exact skill names to publish. An empty list publishes none. |
| exclude | [] | Exact skill names not to publish. Applied after include, so exclusion wins when both name a skill. |
The wholesale replacement a refresh performs is why target must not share a directory with anything else.
Pointing it at a directory OMP scans — ~/.omp/agent/skills, say — looks like it would save a symlink, but the next refresh would stand one repository's download in for the entire directory: another source's skills, the ones you wrote by hand, all gone with it.
Give every source a directory of its own, and let publication be what puts skills where OMP looks.
skills controls what skilld asks gh to download.
Each entry is handed to gh skill install as an exact skill name or repository-relative path, so a nested target such as skills/engineering/reviewer is installed flat as reviewer under the source target just as gh installs it itself.
Targets must be non-empty relative paths with no dot-segments; duplicate targets and different targets that would both install to the same directory name are refused before a refresh starts.
With no skills, the existing gh skill install --all behaviour is unchanged.
include chooses the candidate downloaded skills and exclude removes from that set.
With neither configured, every downloaded skill is published; with both configured, a name in exclude always wins.
Names are exact and case-sensitive.
A missing include name is reported because a typo can silently publish nothing, while a missing exclude name is ignored so stale blacklist entries stay harmless.
Changing include or exclude takes effect on the next launch without another download because those fields control publication only; changing skills changes the next refresh itself, so use /skilld refresh <repository-or-label> if the source is still fresh.
A source pin is passed straight to gh skill install --pin; skilld does not check out refs itself.
Pinning changes what revision a refresh fetches, not when that refresh happens: the source's interval, or the global interval when it has no override, decides normal freshness while failure cooldown and in-flight detection remain independent safeguards.
Set a source's interval to false to make it manual-only; /skilld refresh <repository-or-label> still refreshes it on demand.
Changing a pin on a source that is still fresh therefore waits for its next normal refresh unless you run /skilld refresh <repository-or-label>.
Nothing above is enforced by the settings schema, so everything is validated at runtime.
Anything that does not match is ignored with an error toast rather than taken literally — an option with an unknown name, a sources that is neither a list nor JSON describing one, an entry that names no repo or names something that is not owner/repo, an entry giving interval something other than non-negative milliseconds or false, pin, target, stamp or label the wrong type or an empty string, skills something other than a non-empty array of unique exact skill targets, an include or exclude that is not an array of exact skill names, or a global interval that is not a number.
A ~ on its own, or a leading ~/, is expanded in target and stamp.
Nothing else is — not $VAR, not ~user — because these go straight to mkdirSync and never near a shell.
About placeholder
Repositories started from GitHub's skill template ship a template/ skill described as "Replace with description of the skill and when Claude should use it." It has a description, so nothing filters it out, and a trigger that vague fires on almost anything.
Skilld drops it — but only when the directory still says that about itself, in its frontmatter description; a skill that merely quotes the phrase in its body is not touched.
A repository that ships a real skill called template keeps it, and a template/ with no SKILL.md in it is left alone rather than guessed about.
Point placeholder at a different name if a repository calls its placeholder something else, or set it to false to skip the check entirely.
It has to be a single directory name. A "", a ".", a ".." or anything with a separator in it is refused and nothing is deleted — the deletion is recursive, forced, and aimed inside the finished download, so an empty string would take the whole download with it and a .. would climb out of it entirely.
Finding sources
Discovery ships with the same gh skill preview the refresh depends on:
gh skill search terraform
gh skill preview anthropics/skills pdfThat searches skills, though, and sources takes repositories. Topic search finds those directly — and does not share the Code Search API's ten-a-minute limit:
gh api "search/repositories?q=topic:agent-skills&sort=stars&per_page=20" --jq '.items[] | "★\(.stargazers_count)\t\(.full_name)"'Then inspect the repository before deciding whether to take everything or select exact targets:
gh api repos/<owner>/<repo>/contents/skills --jq '.[] | .type + " " + .name'A small flat collection can stay on the default --all path.
For a large repository, or one that files skills below nested directories such as skills/engineering/<name>/SKILL.md, configure only the names or repository-relative paths you want in skills; skilld passes each target straight to gh skill install and publishes the resulting flat skill directory normally.
Behaviour
- Refreshes each source at most once per its effective
interval: the source override when present, otherwise the global default. The stamp file is written after a refresh succeeds, so an interrupted one simply retries next launch. A source withinterval: falsenever starts an automatic download, but finished staging work is still settled and manual refresh remains available. - Downloads into a hidden staging directory beside
targetand stands it in for the live one only once every configured install target has succeeded, so OMP never scans a half-written skill set. Beside it rather than underTMPDIRbecause a rename across filesystems fails, and hidden so a scan cannot mistake it for a skill. Nothing appears attargetuntil a refresh has actually succeeded. Not a single atomic step — nothing Node exposes can exchange two directories — but the live directory is absent for two renames rather than for the length of a download, and a swap that fails puts the previous skills back rather than leaving a gap. A launch killed between those two renames leaves the previous skills parked beside the target, and the next launch stands them back in rather than sweeping them. - A finished download that could not be installed — the target's parent unwritable, say — keeps its claim, so the next launch retries the install rather than paying for the download again.
- Never awaited, and the download is detached, so quitting OMP never waits on one — and never kills one either. A download that outlives its launch records how it ended beside the staging directory; the next launch installs a finished one instead of downloading it again. Two launches that find the same finished download cannot both install it: claiming it is a single unlink, and the one that loses it stands aside.
- A download still running is left alone, however short the interval. Each download records its process id beside its staging directory, so a launch asks the process itself: alive means left alone however quiet the directory, gone means swept at once — a reboot mid-download, say. Only a download with no pid to ask falls back to the clock: one whose staging area has seen no new file anywhere in fifteen minutes is taken for dead and swept, so the next attempt starts clean.
- A failed download is left alone for an hour before another is attempted, which is what keeps a rate limit or an expired login from costing an attempt per launch. Its staging directory is kept as the record of that failure, and swept when the hour is up. A
ghthat was never found, or could not be run, is exempt: none of the rate limit was spent, so installing it and relaunching refreshes at once instead of an hour later. - Publishes the skills selected by each source's
includeandexcludeinto~/.omp/agent/skillsas one symlink per skill, on every launch rather than only after a download, so changing a selector, deleting a link by hand, or freeing a name takes effect at once. A link into the download root is the plugin's to remove; anything else there is yours and is never touched, which is also how a skill dropped upstream gets its name freed again. - A name already taken by a directory of your own is left alone rather than replaced, and reported to the log. The download still refreshes, so freeing the name is all it takes to get it.
- Nothing throws. A missing
gh, an expired login, a plane — all of them degrade to an error toast, never a broken launch. - A failed download quotes
ghrather than only its exit code. The streamghexplains itself on is kept beside the staging directory, and the end of what it said — a repository that is not there, a login that expired, two skills that would overwrite each other — goes into the toast, the pin and the log alongside the code. The end rather than the start, because a download prints its progress on that same stream and the reason it stopped comes last; trimmed to a line, and cut at the front when it still does not fit, becauseghprints its hint before the reason it is hinting about. Aghthat failed without a word leaves the exit code to speak alone. - Says everything twice on purpose, because the two surfaces lose different things. A toast is shown once and scrolls away with the transcript, which is exactly what a minute-long download outlives; a pin sits in the widget strip under the editor with a glyph and the source's name —
⟳while it downloads,✓with the skill count when it lands,✗with the reason when it does not — and stays there until you start your next turn. A download still running keeps its pin through that turn, since it is not news to be dismissed. The status bar carries the same state in the few columns it has. - Every outcome is written to OMP's log (
~/.omp/logs/omp.<date>.<pid>.log) as well as toasted and pinned, because the launches that need explaining most —omp -p, a CI run — have no TUI for either. - The "in the background" announcement is held back a few seconds, so a refresh that settles within the first beat — a missing
ghfails in milliseconds — speaks for itself instead of arriving after a promise it already broke. Everything else is said the moment it happens; the pin goes up at once, since a pin that is superseded costs a line under the editor rather than a notification. - The sweep runs once per launch, on the first session — subagents do not each get their own refresh.
Windows
An ordinary source with no skills configured keeps the direct gh skill install --all path on Windows, so neither Git Bash nor WSL is involved.
A source with exact skills targets uses one detached PowerShell wrapper to run those gh skill install calls sequentially; the wrapper writes the same completion and failure markers as the POSIX shell wrapper, so the whole selected refresh can outlive OMP and still be picked up by the next launch.
For the direct --all path, completion and failure markers are written by the plugin's own exit handler while OMP is alive.
If that direct child outlives its launch, its result is not recorded and the next launch pays for one redundant download rather than installing it; selected skills refreshes do not have that limitation.
A gh installed only as a .cmd shim cannot be spawned by the direct --all path; that surfaces as a "could not refresh" error toast, and installing the real executable (winget, scoop, the MSI) is the fix.
~ in target and stamp resolves through os.homedir(), which reads USERPROFILE.
Keep the forward slash after the tilde: ~\.local\share\... is not expanded.
Publication uses junctions rather than symlinks, since those need no privileges and OMP's scan reads both the same way.
Development
bun install
bun test
bun run typecheck
bun run lintThere is no build step — OMP loads the TypeScript directly. The suite drives the plugin's own timer queue rather than the clock, so it finishes in well under a second.
