One skill selection for Codex, Claude Code, and Grok, narrowed to the project you are actually in.
skills-mgr finds every SKILL.md your machine already has, records which of
them apply to the current directory, and serves their instructions and scripts
to an agent on demand. A selection entry is a plain on/off switch or a condition
such as lang go && tooling shadowtree, so a Go project and a React project see
different skills without you touching anything. Its terminal interface also
browses and installs skills from skills.sh and
SkillsMP.
TL;DR: run skills-mgr to choose skills and skills-mgr -g to choose them for
every project. A session-start hook feeds skills-mgr list to your agent, which
reads what it picks with get and run.
Requirements:
- Go 1.26 or newer
- Git, when installing a SkillsMP skill
Install the command:
go install github.com/yusing/skills-mgr@latestFrom the project whose selection you want to manage, open the interface:
skills-mgrMove with j/k or the arrow keys and press Space to enable a skill. This
writes .skills-mgr.json in the current directory. Then see what an agent
would see:
skills-mgr list<skills>
<skill name="codebase-design" description="Shared vocabulary for designing deep modules.">
<references>DEEPENING.md
DESIGN-IT-TWICE.md</references>
</skill>
<skill name="writing-for-agents" description="Writing documents for agents.">
<references>SKILL-MECHANICS.md</references>
</skill>
</skills>Read one, or just part of one of its references:
skills-mgr get codebase-design
skills-mgr get codebase-design/DEEPENING.md 1:40To make this automatic in a session, see Agent Integration.
Each agent harness discovers skills from its own directory. A skill you install for Claude Code is invisible to Codex, and the same skill installed three times is three copies to keep current.
A discovered skill is also an active skill. Every harness loads the name and description of everything in its skill directory into every session, whatever the project is. Fifty skills means fifty descriptions in the prompt before the first message, most of them irrelevant to the repository you opened.
skills-mgr separates the two questions those directories conflate:
- Is this skill available? Answered once, per machine, by discovery.
- Does this skill apply right now? Answered per project, by a selection entry that can be a condition rather than a constant.
In a Go repository, a fifty-skill collection typically resolves to a handful, and the agent reads the body of one only when it decides to use it. The selection governs content in the manager's stores, so skills you write belong in the manager home.
Three stages, in order. Every command is one of them.
1. Catalog. Discovery walks a fixed list of roots and collects every
directory holding a valid SKILL.md. Harness-owned roots, the Codex plugin
cache, the manager home, and the remote store all feed one namespace. First
root to declare a name wins. See Skill Discovery.
2. Selection. Two files decide what applies:
$HOME/.skills-mgr/.skills-mgr.json for every project, and ./.skills-mgr.json
for this one. Each entry is true, false, or a Bash expression run from the
project directory. See The Selection File.
3. Access. list advertises the enabled set as XML, get prints a skill
file, and run executes a skill script. get and run refuse a skill that is
not effectively enabled here. See Command Reference.
Content and selection stay separate throughout. Disabling a skill never deletes it, and enabling one never copies it into an agent's directory; it writes only a frontmatter stub the model cannot see. See Where Skill Content Lives.
The two tools solve adjacent problems and compose. Vercel's npx skills
publishes and installs skills; skills-mgr decides which of your installed
skills apply and serves them. skills-mgr installs from skills.sh, Vercel's
registry.
npx skills |
skills-mgr |
|
|---|---|---|
| Primary job | Install and update skills from GitHub repositories | Select among discovered skills and serve them on demand |
| Where content lives | Copied or symlinked into each agent's own skills/ directory |
Two user-level stores; harness directories get frontmatter-only placeholders the model cannot see |
| Turning a skill off | npx skills remove deletes it |
Flip one selection entry; the content stays installed |
| Per-project relevance | Installed means active | true, false, or a Bash condition evaluated in the project directory |
| What the agent sees up front | Every installed skill's description, every session | From skills-mgr, only manager-owned skills enabled here |
| Harness coverage | 76 agents | Codex, Claude Code, Grok |
Six tabs, selected with ← and →:
| Tab | Contents | Network |
|---|---|---|
| Installed | Shared .agents/skills roots, the manager home, and installed remote skills |
None |
| Codex | Codex-native skills. [ and ] cycle the User, Plugin, Builtin, and System sources |
None |
| Grok | Grok-native skills. [ and ] cycle the User, Plugin, and Bundled sources |
None |
| Claude | Claude-native skills. [ and ] cycle the User and Plugin sources |
None |
| skills.sh | Browse topics, search, and install | skills.sh, then its skill-detail API |
| SkillsMP | Browse, search, and install | SkillsMP, then git clone --depth 1 of the skill's repository |
| Key | Action |
|---|---|
← / → |
Change tab |
[ / ] |
Cycle source subtabs in the Codex, Grok, or Claude tab |
j / k, ↓ / ↑ |
Move through results |
f |
Filter local skills, or search the active registry |
| Enter or click | Expand or collapse details |
| Space | Enable or disable the selected skill |
i |
Edit this skill's current-layer enabled value in $EDITOR |
e |
Open an editable local skill's SKILL.md in $EDITOR |
m |
Toggle disable-model-invocation in the skill's own SKILL.md |
a |
Adopt a $HOME/.agents/skills skill into the manager home, or release it back |
u |
Uninstall the selected installed remote skill |
q or Ctrl-C |
Quit |
The i draft accepts true, false, a bare Bash expression, or a JSON string;
saving it empty removes the current-layer value. m rewrites the skill's source
file, preserving the rest of its frontmatter. a moves content rather than
copying it; see The Manager Home.
u deletes stored remote content, so it is refused for a skill configured in
the global layer while you are in project mode; uninstall that one from
skills-mgr -g.
Run the interface with -g to manage the selection that applies everywhere:
skills-mgr -gThis writes $HOME/.skills-mgr/.skills-mgr.json. A file left at the older
$HOME/.skills-mgr.json moves there on the next run, unless one already exists.
In project mode, an entry in ./.skills-mgr.json overrides the global entry
with the same name, and deleting the project entry restores inheritance. The
-g flag must be the only argument; the other commands always act on the
current directory.
Two kinds of skill are enabled even with nothing recorded for them:
- A skill under the project's own
./.agents/skills, on the assumption that a skill committed to a repository belongs to it. - A skill whose frontmatter sets
disable-model-invocation: true. These are omitted fromlistso the agent does not consider them on its own, butgetandrunstill work when you name one.
skills-mgr does not wire itself into a session. Two pieces do that, and every
harness needs both:
- A session-start hook that injects the inventory, so the agent starts the session already knowing what is enabled.
- An instruction file that tells the agent how to read a skill it picks. One text covers all three harnesses.
Run skills-mgr list from a session-start hook and its output becomes part of
the session's opening context. One command serves every harness: list reads
CLAUDECODE, GROK_AGENT or GROK_SESSION_ID, and CODEX_THREAD_ID from the
hook's environment and scopes itself to whichever it finds, so no scope flag is
needed.
Codex, in ~/.codex/hooks.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "skills-mgr list", "timeout": 5 }
]
}
],
"SubagentStart": [
{
"hooks": [
{ "type": "command", "command": "skills-mgr list", "timeout": 5 }
]
}
]
}
}Claude Code takes the same shape in ~/.claude/settings.json, under a
"matcher": "*" wrapper. Grok takes it as a JSON file in ~/.grok/hooks/.
The inventory has to be supplied when a session begins, when a subagent starts with a fresh context, and after compaction. Register whichever events the harness offers:
| Harness | Events |
|---|---|
| Codex | SessionStart, SubagentStart |
| Claude Code | SessionStart, SubagentStart, PostCompact |
| Grok | SessionStart, PostCompact |
Codex runs SessionStart again after compaction with source: "compact", before
the next model request, so its existing hook restores the inventory. Claude Code
and Grok use PostCompact for the same job.
Pass an explicit --claude, --grok, or --codex only where the environment
cannot answer the question: a wrapper that clears it, or a session that sets
more than one marker. list hides nothing when it cannot identify exactly one
harness.
Because the hook supplies the inventory, this text does not ask the agent to run
list itself. Put it in ~/.codex/AGENTS.md, ~/.claude/CLAUDE.md, and
~/.grok/AGENTS.md:
## Skills
Read a skill whose listed description matches the operation you are about to
perform, just in time and exactly once per context. Start with the most
specific owner, and add another only when it covers a separate responsibility.
Keep a loaded skill active across follow-ups.
Read a skill's instructions with `skills-mgr get <skill-name> [start:end]`, and
a listed reference with `skills-mgr get <skill-name>/<relative-path>
[start:end]`. Omit the optional 1-based inclusive range to read the whole file,
and load only the references you actually need.
Run scripts with `skills-mgr run <skill-name>/<relative/script> [args...]`.
If a skill is missing or unreadable, say so briefly and carry on.That is the minimum that makes the command usable. Selection policy beyond it is yours to add: how to arbitrate between overlapping workflow skills, and when a delegated agent owns its own skills.
Claude Code and Grok need nothing beyond the instruction file and the hook. For every harness, the manager's selection governs skills adopted into its home; skills left in harness-owned locations stay outside that selection.
| Harness | Instruction file | Skills outside the manager selection |
|---|---|---|
| Codex | ~/.codex/AGENTS.md |
Skills kept in Codex-owned locations or installed through plugins |
| Claude Code | ~/.claude/CLAUDE.md |
Skills left in .claude/skills or ~/.claude/skills |
| Grok | ~/.grok/AGENTS.md |
Skills left in .grok/skills, ~/.grok/skills, the shared .agents/skills roots, or the .claude/skills roots |
Grok scans the Claude directories for compatibility. Set
GROK_CLAUDE_SKILLS_ENABLED=false and skills-mgr stops crediting Grok with
them. The equivalent [compat.claude] setting in Grok's config.toml is not
read, so turning it off there means a skill is named to the agent twice rather
than withheld from it. Because Grok claims the shared roots too,
skills-mgr list --grok reports little more than the installed remote skills.
Codex is the one harness that needs its own loader turned off, because it ships
skill instructions of its own that would compete with the text above. In
config.toml:
model_instructions_file = "/path/to/your/base_instructions.md"
[skills]
include_instructions = falseThe stock base prompts are published at
openai/codex
and in
system_prompts_leaks.
Start from one and delete its ## Using skills section, leaving nothing in its
place.
Every command uses the current working directory as the project.
| Command | Result |
|---|---|
skills-mgr |
Open the project selection interface |
skills-mgr -g |
Open the global selection interface |
skills-mgr help |
Print every accepted invocation form |
skills-mgr adopt |
Move all valid shared skills from $HOME/.agents/skills into the manager home |
skills-mgr list |
Write the enabled skills to stdout as XML: name, description, and reference-file tree |
skills-mgr get <skill> |
Write the body of the skill's SKILL.md to stdout |
skills-mgr get <skill>/<path> |
Write a file from the skill to stdout |
skills-mgr get <skill>/<path> <start>:<end> |
Write an inclusive, 1-based line range |
skills-mgr run <skill>/<script> [args...] |
Run a script from the skill, passing through its standard streams and exit status |
skills-mgr sync |
Fetch the enabled remote skills this project references but this machine does not have |
skills-mgr daemon |
Refresh registry metadata and stale remote content until interrupted |
skills-mgr daemon refresh |
Ask the running daemon to refresh the skills.sh registry cache now |
skills-mgr daemon sync |
Ask the running daemon to update stale persisted remote skills now |
get and run reject a skill that is not effectively enabled in this project.
list, get, and run accept the --claude, --grok, and --codex flags
described under Agent Integration, though only list
filters on them.
get omits YAML frontmatter from Markdown files, and a line range applies to
that frontmatter-free content.
run uses the skill directory as the script's working directory. Executable
files run directly, non-executable .py files run under python3, and
non-executable .js, .mjs, .cjs, .ts, .mts, and .cts files run under
the first of node and bun found in PATH.
# Read an enabled skill's main instructions.
skills-mgr get writing-readme
# Read part of a reference file.
skills-mgr get deliver-vertical-slice/references/validation-scenarios.md 10:30
# Run a script the skill ships, passing arguments through to it.
skills-mgr run go-microoptimizations/scripts/go_asm_metrics.py ./...A skill is a directory holding a SKILL.md whose YAML frontmatter carries a
valid name and a non-empty description. Names may contain letters, digits,
., _, -, and :, up to 80 characters.
Discovery reads these roots in order, and the first one to declare a given name owns it:
| Priority | Root | Reported source |
|---|---|---|
| 1 | ./.agents/skills |
project |
| 2 | $HOME/.agents/skills |
user |
| 3 | $HOME/.skills-mgr/skills |
managed |
| 4 | ./.claude/skills |
claude |
| 5 | ./.grok/skills |
grok |
| 6 | ./.codex/skills |
codex |
| 7 | $CODEX_HOME/skills, or $HOME/.codex/skills when CODEX_HOME is unset |
codex |
| 8 | /etc/codex/skills |
admin |
| 9 | skills directories in $CODEX_HOME/plugins/cache, up to 10 levels deep |
plugin |
| 10 | The skills-mgr remote store |
The provider name |
A .system subdirectory inside either Codex root is scanned as well and
reported as bundled. Missing, unreadable, or invalid entries are skipped
silently, as are the placeholder directories skills-mgr maintains for
autocomplete.
Grok Plugin and Bundled rows use metadata from grok inspect --json. Space on
one of those rows changes the global skills.disabled list in
$HOME/.grok/config.toml; Grok does not support a project-local override for
these skills. The other status and source-editing keys do not apply to those
rows. Claude Plugin rows read their status from enabledPlugins in
$HOME/.claude/settings.json and are display-only.
The interactive interface also reads installed Claude plugin skill directories
from $HOME/.claude/plugins/installed_plugins.json and Grok Plugin and Bundled
skills from grok inspect --json. These harness-owned catalogs are displayed
only in their nested TUI tabs. They are never emitted by skills-mgr list,
regardless of their displayed status.
Roots 4 through 9 belong to a specific harness. Under a --claude, --grok,
or --codex scope, roots owned by a different harness are dropped from the
catalog entirely. Roots assigned to the scoped harness remain in its catalog
but are omitted from list; selection takes effect after a skill is adopted
into the manager home.
.skills-mgr.json is strict JSON at schema revision 3. Revisions 1 and 2
still load and are upgraded on the next selection write. The full contract is
in skills-mgr.schema.json.
{
"schema_revision": 3,
"skills": {
"writing-readme": {
"enabled": true
},
"unused-skill": {
"enabled": false
},
"golang-best-practices": {
"enabled": "lang go"
}
}
}Prefer the interactive interface for edits. Configured names that no longer resolve to a discovered skill are retained and ignored. Coordination locks live in the user cache, not the project directory, so nothing extra appears in your working tree.
Commit .skills-mgr.json when a repository should share its selection.
An
enabledstring runs as Bash, with your environment and filesystem permissions, whenever the skill's state is checked. Review committed.skills-mgr.jsonchanges before runningskills-mgrin a repository you do not trust.
A string value is parsed as Bash by mvdan.cc/sh and
evaluated from the project directory. Final status 0 enables the skill,
status 1 disables it, and anything from 2 to 255 is an error. External
commands run normally, so command -v herdr works as a condition.
Three builtins answer the common questions. All three read one shared scan of the project described under Evaluation Cost, and none of them starts a process.
Tests for a dependency declaration in any go.mod, Cargo.toml, or
package.json the scan reached. Operators are >=, ==, <=, and <. A
quoted version argument may combine comparisons with && and ||; && binds
tighter, and every comparison in an && group must match the same declaration.
Comparisons use the precision you supply, so ==2 matches any version in major
2, while ==2.1 also compares the minor version.
Indirect Go requirements are ignored. Cargo dependency, dev-dependency,
optional-dependency, and [workspace.dependencies] catalog entries all count,
even when no member inherits them. A declaration with no version, such as a
Cargo path dependency, matches only the name-only form.
Version checks read the numeric boundaries written in a declaration rather than fully interpreting npm or Cargo range semantics, so exact declarations are the most predictable.
has_dependency tauri '>=2 && <3' || has_dependency '@tauri-apps/api' '>=2 && <3'Tests whether the project contains a matching package marker or file extension. A mixed-language project satisfies every language present. Names and aliases are lowercase.
| Language | Package markers and file extensions |
|---|---|
go |
go.mod, .go |
rust |
Cargo.toml, .rs |
node |
package.json |
typescript / ts |
tsconfig.json, .ts, .tsx, .mts, .cts |
tsx |
.tsx |
javascript / js |
.js, .jsx, .mjs, .cjs |
jsx |
.jsx |
html |
.html, .htm |
css |
.css |
python |
pyproject.toml, requirements.txt, Pipfile, .py, .pyw |
c |
.c |
c++ |
.C, .cc, .cpp, .cxx, .c++, .hh, .hpp, .hxx |
c# |
.cs, .csproj |
java |
.java |
lua |
.lua |
vb |
.vb, .vbproj |
php |
composer.json, .php |
r |
.r, .rmd, .rproj, matched case-insensitively |
ruby |
Gemfile, .rb |
swift |
Package.swift, .swift |
perl |
cpanfile, .pl, .pm |
assembly / asm |
.asm, .s, matched case-insensitively |
shell / sh |
.sh |
bash |
.bash |
postgres |
postgresql.conf, pg_hba.conf, pg_ident.conf, .psql |
sql |
.sql |
yaml |
.yaml, .yml |
json |
.json |
toml |
.toml |
ini |
.ini |
One file can detect several languages: Cargo.toml detects rust and toml,
and package.json detects node and json.
Tests whether the project contains a conventional lockfile, configuration file, or build entrypoint for a tool. This is evidence in the repository, not whether the executable is installed.
| Tool | Project evidence |
|---|---|
bun |
bun.lock, bun.lockb, bunfig.toml |
yarn |
yarn.lock, .yarnrc, .yarnrc.yml, .yarnrc.yaml |
deno |
deno.json, deno.jsonc, deno.lock |
npm |
package-lock.json, npm-shrinkwrap.json |
pnpm |
pnpm-lock.yaml, pnpm-workspace.yaml |
maven |
pom.xml, mvnw, mvnw.cmd |
composer |
composer.json, composer.lock |
cmake |
CMakeLists.txt, CMakePresets.json, CMakeUserPresets.json |
make |
Makefile, makefile, GNUmakefile |
just |
justfile, Justfile, .justfile |
shadowtree |
.shadowtree.toml |
taskfile |
Taskfile.yml, Taskfile.yaml, taskfile.yml, taskfile.yaml |
bazel |
.bazelrc, MODULE.bazel, WORKSPACE, WORKSPACE.bazel, BUILD, BUILD.bazel |
docker |
Dockerfile, Dockerfile.*, docker-bake.hcl, docker-bake.json, or Docker Compose evidence |
docker-compose |
compose.yml, compose.yaml, docker-compose.yml, docker-compose.yaml |
kubernetes / k8s |
kustomization.yml, kustomization.yaml, Kustomization, Chart.yaml, skaffold.yml, skaffold.yaml |
pip |
pip.conf, pip.ini, or a requirements*.txt file |
uv |
uv.lock, uv.toml |
has_dependency, lang, and tooling share one scan of the project. It runs
at most once per command, however many skills use a condition, and directory
reads run on a bounded worker pool.
The scan honors Git's ignore rules, so an ignored file is not evidence. It
applies .git/info/exclude and every .gitignore from the enclosing worktree
root down through the directories it visits, while a file recorded in the Git
index counts even when a rule would otherwise ignore it. Repository metadata is
read in process, so no git executable is involved. Outside a worktree, or when
that metadata cannot be read, in-tree .gitignore files still apply. .git,
node_modules, and target are always skipped, and unreadable directories are
passed over.
Two consequences are worth knowing:
- Run from
$HOME, only its direct entries count. Home is where you run commands, not one project containing every checkout beneath it. - A project directory that its parent repository ignores, and does not track, produces no evidence, so every condition on it is false.
lang ts && (tooling pnpm || tooling bun)skills-mgr owns two stores, and content in either one is governed by the
selection because no harness scans them.
Skills you write belong in $HOME/.skills-mgr/skills. Press a on the
Installed tab to move one there from $HOME/.agents/skills, and a again to
move it back.
That move is what makes a selection entry mean anything for a skill you wrote.
Grok scans the shared .agents/skills roots, so a skill sitting there loads
into every session whatever its entry says. Claude Code does not scan them at
all, so the same skill is missing from its slash-command menu. Placeholders fix
both.
Adoption writes the placeholder after the move; release removes it first,
because in global mode the release destination is the placeholder's own path.
e still edits the content, and u refuses it.
Skills installed from skills.sh or SkillsMP live under the user cache directory,
in skills-mgr/remote-skills, because sync can refetch them. Authored content
cannot be refetched, which is why the manager home sits outside the cache.
Content is never executed at install time.
Registry responses are cached alongside the content. The SkillsMP client sends
SKILLSMP_API_KEY as a bearer token when that variable is set, and makes
unauthenticated requests otherwise. Provider results are validated before they
reach the store: absolute or escaping paths, non-regular entries, duplicate
paths, responses over 16 MiB, and a root SKILL.md whose name does not match
the catalog entry are all rejected.
Enabling a skill from either store writes frontmatter-only placeholders under
.agents/skills/ and .claude/skills/, or under $HOME in global mode, so the
harness can offer the name. Disabling removes the placeholders and leaves the
content in place.
Every placeholder sets disable-model-invocation: true, so the harness offers
the name in its slash-command menu for you while withholding the description
from the model. skills-mgr list stays the only model-facing view of what
applies here. Two consequences follow: a conditional entry gets a placeholder
like any other, because a stub the model cannot see cannot bypass a false
condition, and a leftover placeholder that a project override cannot delete
under $HOME costs the model nothing. list, get, and run honor the
project override regardless.
A skill outside both stores gets no placeholder. It already occupies its harness-owned path, and a stub there would collide with the real directory.
A committed .skills-mgr.json records each remote skill's provider, provider
ID, name, and locator, which is everything needed to refetch it. On another
machine:
skills-mgr syncsync matches project and inherited global names against the local store,
fills in any missing identity metadata, fetches every effectively enabled
remote that is absent or stale, and prints one line per skill synchronized. It
never fetches a disabled entry, and a failure names the skill, returns non-zero,
and leaves both selection files unchanged.
Nothing else downloads on your behalf. list, get, run, and TUI startup
will not fetch a referenced remote skill they have never seen.
skills-mgr daemon listens on $XDG_RUNTIME_DIR/skills-mgr.sock, falling back
to skills-mgr.sock in the user cache skills-mgr directory. It refreshes
skills.sh registry metadata at startup and every five minutes, and refreshes
installed remote content once it passes three hours old. Structured text logs go
to stderr for start, stop, inbound commands, cache refresh, and each
remote-skill update; a failure is logged and does not stop later cycles.
skills-mgr daemon refresh and skills-mgr daemon sync ask that process to
refresh the registry cache or update stale remote skills right now. Both wait
for the work to finish, both fail if no daemon is running, and neither
downloads a remote identity the store has not seen.
To install it as a systemd user unit:
shadowtree install-systemdThat recipe runs shadowtree install first, so the binary lands wherever
GOBIN points, defaulting to $HOME/go/bin. That is the path
skills-mgr.service names in ExecStart. If you set
GOBIN somewhere else, edit ExecStart to match before enabling the unit.
Design notes for the remote-selection subsystem live in
doc/brief.md, doc/spec/index.md, and
doc/architecture/index.md.
The repository uses Shadowtree recipes.
git clone https://github.com/yusing/skills-mgr.git
cd skills-mgr
shadowtree check| Workflow | Command | Result |
|---|---|---|
| Build | shadowtree build |
Compile all Go packages in a sandbox |
| Test | shadowtree test |
Run the Go test suite |
| Vet and test | shadowtree check |
Run go vet, then the test suite |
| Race detection | shadowtree test-race |
Run tests with the race detector |
| Lint | shadowtree lint |
Run golangci-lint |
| Format | shadowtree fmt |
Format Go source in the checkout |
| Tidy | shadowtree tidy |
Tidy go.mod and go.sum |
| Install checkout | shadowtree install |
Install the main package with debug information stripped |
| Install service | shadowtree install-systemd |
Install to GOBIN and enable the systemd user unit |
MIT. See LICENSE.