ASCII diagrams + prose covering:
- Storage model: one parked source-of-truth fanned out to 3 lazy mirrors
- Data flow for enable/disable (incremental INDEX updates, no global reindex)
- INDEX.json schema with per-target active[] array
- Interactive pick / disable-pick flows incl. target-picker UX
- The fzf {} shell-escape gotcha and the correct quoting pattern
opc-skills
Multi-target skill manager for opencode + Claude Code + Feynman. Single source-of-truth (parked archive in the personas repo), per-target lazy activation. Default operations target all three platforms; an interactive target-picker prompts when picking via fzf.
Why
opencode (≥1.14) auto-scans ~/.claude/skills/ as well as its own ~/.config/opencode/skills/ — so a 1000-skill Claude Code archive ends up bloating the opencode prompt. Feynman has its own ~/.feynman/agent/skills/. opc-skills inverts this: keep all three target dirs empty by default, copy individual skills into them on demand.
For full agent / skill / persona generation across multiple AI platforms, see the upstream personas repo.
Architecture
Storage model — one source-of-truth, three lazy mirrors
┌─────────────────────────────────────────┐
│ ~/Documents/personas/skills-archive/ │
│ (single source-of-truth, 1072+ skills) │
│ │
│ <skill>/SKILL.md ← frontmatter: │
│ name, desc, │
│ domain, sub, │
│ tags │
│ INDEX.json ← per-target active map │
│ INDEX.md ← human-readable table │
└─────────────────┬───────────────────────┘
│
│ cp -r (lazy, on demand,
│ per --target)
▼
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ opencode │ │ claude │ │ feynman │
│ ~/.config/ │ │ ~/.claude/ │ │ ~/.feynman/ │
│ opencode/ │ │ skills/ │ │ agent/ │
│ skills/ │ │ │ │ skills/ │
└───────────────┘ └───────────────┘ └───────────────┘
The parked archive is the only dir that's authoritative. The three target dirs are mutable mirrors — enable copies in, disable removes from. No skill is ever deleted from parked by opc-skills.
Data flow — enable / disable
enable <name> --target T1,T2,...
│
├──► for each target T:
│ cp -r PARKED/<name> → ACTIVE_T/<name>
│ sync shared refs (e.g. _platform-mapping.md)
│ INDEX.json: append T to record's `active[]` array
│
└──► no global reindex required (incremental update)
disable <name> --target T1,T2,...
│
├──► for each target T:
│ rm -rf ACTIVE_T/<name>
│ INDEX.json: remove T from `active[]`
│
└──► PARKED copy stays intact
INDEX schema
Each entry is a deduped record across all 3 targets:
{
"folder": "performing-network-traffic-analysis-with-tshark",
"name": "performing-network-traffic-analysis-with-tshark",
"description": "Automate network traffic analysis using tshark…",
"domain": "cybersecurity",
"subdomain": "network-security",
"tags": ["network-analysis", "pcap", "tshark"],
"active": ["claude", "feynman"], // per-target presence
"status": "active" // "active" if any target, else "parked"
}
reindex rebuilds this from filesystem state across all 4 dirs (parked + 3 actives). enable/disable mutate active[] and status incrementally so bulk axis-ops (enable-domain, enable-tag) stay accurate without rescanning.
Interactive flows (pick / disable-pick)
pick: disable-pick:
───── ────────────
① fzf: pick category ① fzf: pick category
(verb-prefix: performing, (from union-of-actives across
detecting, analyzing…) opencode + claude + feynman)
② fzf --multi: pick skills ② fzf --multi: pick skills
preview: PARKED/<skill>/SKILL.md preview: same
[TAB toggles, ENTER confirms] rows annotated [oc,cl,fy]
③ fzf: pick target(s) ③ fzf: pick target(s) to remove from
┌───────────────────────────┐ same picker
│ all (default) │ ENTER on highlighted "all"
│ opencode (N active) │ = remove from every target
│ claude (N active) │ where it's currently active
│ feynman (N active) │
└───────────────────────────┘
[TAB multi-select supported]
④ cp -r per-target ④ rm -rf per-target
The target picker leverages an fzf quirk: --multi returns the highlighted line as the sole selection if the user never pressed TAB. Putting all first means a default ENTER falls through to all three targets.
fzf {} quoting (gotcha)
fzf shell-escapes {} substitutions automatically (wraps in single quotes). Wrapping {} again in outer quotes embeds those quote literals into the path:
# ❌ becomes: sed -n '1,40p' ".../skills-archive/'name'/SKILL.md"
--preview="sed -n '1,40p' \"$PARKED/{}/SKILL.md\" 2>/dev/null"
# ✅ becomes: sed -n '1,40p' ".../skills-archive"/'name'/SKILL.md
# (bash concatenates adjacent quoted/unquoted strings cleanly)
--preview="sed -n '1,40p' \"$PARKED\"/{}/SKILL.md 2>/dev/null"
All preview lines in bin/opc-skills follow the second pattern.
Install
ln -s ~/Documents/opc-skills/bin/opc-skills ~/.local/bin/opc-skills
Ensure ~/.local/bin is on your PATH.
Layout it expects
~/Documents/personas/skills-archive/ # full catalog — single source-of-truth
<skill-name>/SKILL.md
INDEX.json
INDEX.md
~/.config/opencode/skills/ # currently-enabled (opencode target)
<skill-name>/SKILL.md
~/.claude/skills/ # currently-enabled (claude target)
<skill-name>/SKILL.md
~/.feynman/agent/skills/ # currently-enabled (feynman target)
<skill-name>/SKILL.md
(~/Documents/opencode-skills-parked is symlinked to personas/skills-archive for backward-compat.)
Override via env:
OPC_PARKED— parked catalog root (default~/Documents/personas/skills-archive)OPC_ACTIVE— opencode active dir (default~/.config/opencode/skills)OPC_CLAUDE_ACTIVE— claude active dir (default~/.claude/skills)OPC_FEYNMAN_ACTIVE— feynman active dir (default~/.feynman/agent/skills)OPC_TARGETS— comma-separated default targets (defaultopencode,claude,feynman)
Targets
enable, disable, and disable-all accept --target / -t:
| flag | meaning |
|---|---|
--target opencode |
only ~/.config/opencode/skills/ |
--target claude |
only ~/.claude/skills/ |
--target feynman |
only ~/.feynman/agent/skills/ |
--target all |
all three |
--target both |
legacy opencode,claude (no feynman) |
--target opencode,feynman |
comma-separated subset |
When omitted, the default is taken from OPC_TARGETS (default opencode,claude,feynman).
Commands
Inspection
opc-skills status # parked + per-target active counts (3 targets)
opc-skills list {active|parked|all} # skill folder names
opc-skills categories # verb-prefix counts (performing, detecting, ...)
opc-skills domains # frontmatter `domain:` distribution
opc-skills subdomains # frontmatter `subdomain:` distribution
opc-skills tags # frontmatter `tags:` distribution
Single-skill operations
opc-skills enable [--target T] <folder> # copy parked → active in selected targets
opc-skills disable [--target T] <folder> # remove from active in selected targets
opc-skills disable-all [--target T] [-y|--yes] # disable every active skill across targets
Bulk by axis
opc-skills enable-category <prefix> # fzf multi-pick within a verb-prefix
opc-skills disable-category <prefix>
opc-skills enable-domain <domain> # bulk by frontmatter domain (eg. cybersecurity)
opc-skills disable-domain <domain>
opc-skills enable-subdomain <subdomain> # bulk by subdomain (eg. malware-analysis)
opc-skills disable-subdomain <subdomain>
opc-skills enable-tag [--all] <tag>... # bulk by tags (default: any; --all: intersection)
opc-skills disable-tag [--all] <tag>...
Interactive / search
opc-skills pick # fzf: category → multi-select skills → target picker → enable
opc-skills disable-pick # fzf: union-of-actives → category → multi-select → target picker → disable
opc-skills search [query] # fzf fuzzy over name+description (enables on selection)
opc-skills disable-search [query] # fzf over ACTIVE only (disables on selection)
opc-skills reindex # rebuild INDEX.json / INDEX.md
pick flow
- Choose a verb-prefix category (
performing,detecting, …) - fzf shows skills in that category —
TABtoggles,ENTERconfirms - Target picker appears:
all/opencode/claude/feynman—TABfor multi-select;ENTERon highlightedall(default) sends to all three - Selected skills are enabled in the chosen target(s)
disable-pick flow
Now multi-target aware: lists the union of all active skills across the three targets, with each entry annotated [oc,cl,fy] showing where it's currently active. After multi-select, the same target picker decides which target(s) to remove from.
fzf is required for the interactive commands; jq for the bulk-by-axis commands; python3 (with optional PyYAML) for reindex.
INDEX schema
reindex parses each SKILL.md frontmatter and emits records with: folder, name, description, domain, subdomain, tags[], active[] (per-target list — opencode/claude/feynman), status. enable / disable perform an incremental status update on INDEX.json so enable-domain / disable-tag etc. stay accurate without a full reindex.
Shared reference files
Some skills reference sibling files via ../<name>.md (e.g. the 20 Feynman research skills use ../_platform-mapping.md for cross-platform tool mapping). opc-skills enable auto-syncs any such files from PARKED → ACTIVE in all three targets so the relative references keep resolving.
Currently synced:
_platform-mapping.md— Feynman-skills cross-platform subagent/scheduling/persistence mapping
If you add new shared-reference files at the PARKED root, extend sync_shared_refs_to_targets() in bin/opc-skills.
Implementation notes
fzfplaceholder{}is shell-escaped automatically — preview commands use"$PARKED"/{}/SKILL.md(variable quoted,{}unquoted) so bash concatenation produces a clean path. Wrapping{}in extra quotes ("$PARKED/{}/SKILL.md") embeds fzf's escape literals into the path and breaks preview.
Known interaction
The personas repo's build.py --install opencode currently wipes and repopulates ~/.config/opencode/skills/ as part of installation, which destroys the active set curated here. Use opc-skills after any personas install, or skip the skills half of build.py installs.
build.py --install opencode and build.py --install claude-skills both emit _platform-mapping.md at the skills-root alongside the 20 Feynman skills (deep-research, literature-review, paper-code-audit, peer-review, paper-writing, replication, source-comparison, summarize, alpha-research, eli5, autoresearch, docker, modal-compute, runpod-compute, session-log, session-search, jobs, watch, preview, contributing).