@lostgradient/skillset
v0.0.7
Published
Compile one canonical set of agent skills, subagents, MCP servers, instructions, hooks, and model defaults into Claude Code and OpenAI Codex configuration.
Downloads
464
Readme
skillset
A CLI that maintains one canonical set of agent configuration — skills, subagents, MCP servers, instructions files, lifecycle hooks, and model defaults — and compiles it into the dialects Claude Code and OpenAI Codex each understand.
Author everything once under a single source root; skillset emits per-tool output, translating Claude-only dynamic features into prose fallbacks for Codex. User scope (default) writes into your home directory; --scope project writes the same kinds into the current repo:
| Source | Claude (user) | Codex (user) | Claude (project) | Codex (project) |
| -------------------------- | -------------------------------- | --------------------------------------------------- | ----------------------- | -------------------- |
| ./skills/<name>/SKILL.md | ~/.claude/skills/<name>/ | ~/.agents/skills/<name>/ (+ agents/openai.yaml) | .claude/skills/ | .agents/skills/ |
| ./agents/<name>.md | ~/.claude/agents/<name>.md | ~/.codex/agents/<name>.toml | .claude/agents/ | .codex/agents/ |
| ./mcp-servers.yaml | mcpServers in ~/.claude.json | [mcp_servers.*] in ~/.codex/config.toml | .mcp.json | .codex/config.toml |
| ./instructions.md | ~/.claude/CLAUDE.md | ~/.codex/AGENTS.md | CLAUDE.md | AGENTS.md |
| ./hooks.yaml | ~/.claude/settings.json | ~/.codex/hooks.json | .claude/settings.json | .codex/hooks.json |
| ./defaults.yaml | ~/.claude/settings.json | ~/.codex/config.toml | .claude/settings.json | .codex/config.toml |
The source root is the current directory, or $SKILLSET_DIRECTORY when set (configuration resolves through @lostgradient/environmentalist, so .env files and skillset.config.* files work too). Any kind may be absent.
Usage
skillset [sync] [--dry-run] [--prune] [--force] [--scope user|project] [--target claude|codex] [--kind <kind>] [--json]
skillset doctor [--targets] [--json]
skillset list [--json]
skillset show <name> [--target claude|codex] [--json]
skillset new <skill|agent> <name>
skillset remove <skill|agent> <name>
skillset get <skill|agent> <name> [<field-path>] [--json]
skillset set <skill|agent> <name> <field-path> <value>
skillset import <skill|agent|instructions> [name] [--from claude|codex]
skillset mcpsync(the default) compiles every source into both tools. It only ever overwrites or removes what it previously generated: file outputs carry agenerated by skillsetmarker, config entries are tracked in the ledger at~/.config/skillset/state.json(with content hashes and timestamps). Hand-installed entries are skipped as unmanaged; managed outputs that were hand-edited since the last sync are skipped as drifted — both with loud warnings, both overridable with--force. Shared config files are backed up to<file>.skillset-backupbefore the first write of a run.doctorvalidates every source and exits non-zero on errors;syncrefuses to write while any error remains.doctor --targetsflips direction: it audits the ledger against disk and reports every managed output asclean,drift, ormissing— run it before syncing on a machine where you may have hand-tweaked compiled output.importadopts an existing hand-installed item: it reverse-compiles the installed skill/agent/instructions file into a source (Codex agent TOML becomes union frontmatter + body;agents/openai.yamlfolds into theopenai:block), then force-syncs that one item so the pre-existing targets gain markers and ledger entries.- The CRUD commands exist so a coding agent can manage sources without hand-parsing directories:
listfor inventory + status,showfor a compiled preview,new/removefor scaffolding and deletion, andget/setfor frontmatter fields via dot paths.setvalues are parsed as YAML and the write is schema-validated first. mcpserves every operation above as an MCP tool over stdio (list_sources,run_doctor,check_targets,sync,show_source,new_source,remove_source,get_field,set_field,import_source) — register it withclaude mcp add skillset -- skillset mcporcodex mcp add skillset -- skillset mcpso an agent can manage its own configuration directly, with the same ownership/drift rules as the CLI.- Every command supports
--json(stable shapes, documented indocumentation/ui-readiness.md); text output respectsNO_COLOR/FORCE_COLORand disables color on non-TTY pipes. Runskillset <command> --helpfor the full reference on any command.
Skills (./skills/<name>/SKILL.md)
One frontmatter block holds the union of what both tools support:
---
name: my-skill # shared, must match the directory name
description: What and when. # shared
license: MIT # shared spec fields
allowed-tools: Read, Grep # shared
model: sonnet # Claude-only fields → Claude output only
when_to_use: ...
disable-model-invocation: true
openai: # Codex-only → emitted as agents/openai.yaml
interface:
display_name: 'My Skill'
short_description: ...
policy:
allow_implicit_invocation: false
---The full Claude-only set: when_to_use, argument-hint, disable-model-invocation, user-invocable, disallowed-tools, model, effort, context, agent, background, hooks, paths, and shell. Shared fields (name, description, arguments, allowed-tools, plus the agentskills.io spec's license/compatibility/metadata) are emitted to both tools. The openai block supports interface (display_name, short_description, icon_small, icon_large, brand_color, default_prompt), policy.allow_implicit_invocation, and dependencies.tools. disable-model-invocation: true implies policy.allow_implicit_invocation: false unless an explicit openai.policy is set.
Supporting files (scripts/, references/, assets/, …) are copied verbatim into both targets. See skills/example-skill/.
Agents (./agents/<name>.md)
Claude-style subagent markdown — frontmatter plus a body that becomes the system prompt:
---
name: reviewer
description: Reviews diffs.
tools: Read, Grep # becomes prose in the Codex output (set codex.tools for the native form)
model: haiku # Claude model family
permissionMode: plan # plan → read-only, acceptEdits → workspace-write
codex: # Codex models are a different family, so they're explicit
model: gpt-5.6-luna
model_reasoning_effort: low
sandbox_mode: read-only # explicit value wins over permissionMode mapping
---Claude gets the file nearly verbatim at ~/.claude/agents/<name>.md (supported fields: tools, disallowedTools, model, permissionMode, maxTurns, skills, mcpServers, hooks, memory, background, effort, isolation, color, initialPrompt). Codex gets ~/.codex/agents/<name>.toml with the body as developer_instructions (Codex fallbacks applied), tools/disallowedTools folded in as a "Tool guidance" prose section, and the codex: block's settings (model, model_reasoning_effort, model_verbosity, sandbox_mode, nickname_candidates).
Codex agents also natively support hooks, mcp_servers, skills, and tools tables — but with different schemas from Claude's same-named frontmatter, so skillset does not auto-translate them: set codex.hooks / codex.mcp_servers / codex.skills / codex.tools and they are emitted verbatim as TOML tables (doctor reminds you when the Claude-side field is set without its Codex counterpart). Only maxTurns, memory, background, isolation, and initialPrompt have no documented Codex equivalent and are dropped with a warning. See agents/example-agent.md.
MCP servers (./mcp-servers.yaml)
YAML because it is one authoring language across all skillset sources and keeps comments; both targets are transformations anyway (Claude speaks JSON, Codex speaks TOML).
servers:
neon:
url: https://mcp.neon.tech/mcp # url → http transport, command → stdio
headers:
Authorization: 'Bearer ${NEON_KEY}' # → bearer_token_env_var for Codex
timeout: 600000 # Claude ms → Codex tool_timeout_sec
claude: { oauth: { callbackPort: 8080 } } # merged into the Claude entry
codex: { startup_timeout_sec: 120 } # merged into the Codex section; winsCodex mappings: ${VAR}-only header values → env_http_headers, static headers → http_headers, ${VAR}-only env values → env_vars (literals stay in env). SSE and WebSocket transports are rejected at the source level — Claude still accepts them, but Codex supports only stdio and streamable HTTP, and a union source must compile for both. Claude-only extras (headersHelper, oauth, …) go through the claude: block; Codex-only extras (auth, required, enabled_tools, …) through codex:. Writes are surgical: the Claude edit touches only the managed mcpServers entries in ~/.claude.json; the Codex edit splices only the managed [mcp_servers.<name>] line spans in ~/.codex/config.toml, so comments and formatting elsewhere survive byte-for-byte.
Instructions (./instructions.md)
Plain markdown compiled to CLAUDE.md (Claude) and AGENTS.md (Codex), with the same #if directives for provider-specific sections. No fallback rewriting applies — instructions have no inline-shell or argument semantics — but doctor warns when a Claude @path memory import sits outside an #if claude guard, since AGENTS.md has no import syntax.
Hooks (./hooks.yaml)
Both tools use a near-identical lifecycle-hook config shape, so one source compiles to both — the hooks key of Claude's settings.json and Codex's hooks.json:
hooks:
PreToolUse:
- matcher: Bash # optional regex/matcher
command: ./scripts/check.sh
timeout: 10 # seconds, in both tools
statusMessage: Checking…
claude: { async: true } # per-target handler overrides, merged last
codex: { timeout: 20 }
FileChanged:
- command: ./scripts/watch.sh
targets: [claude] # Claude-only event — restriction requiredCodex supports 11 events (SessionStart/End, PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit, Stop, PreCompact/PostCompact, SubagentStart/Stop), all of which exist in Claude's larger set; doctor errors when a Codex-targeted hook uses a Claude-only event. Ownership is entry-level via the ledger: hand-written hooks in the same files are never touched, and a managed entry you hand-edit is treated as drifted. Heads-up on every Codex hook write: Codex trust-hashes its hook config, so changed hooks must be re-trusted via /hooks.
Defaults (./defaults.yaml)
Model/effort defaults, applied as surgical key-level edits (model/effortLevel in Claude's settings.json; top-level model/model_reasoning_effort/model_verbosity scalars in Codex's config.toml, comments preserved). Keys the user set by hand are skipped as unmanaged.
claude: { model: opus, effort: high }
codex: { model: gpt-5.6-sol, model_reasoning_effort: high }Templating
Target-conditional blocks work in skill and agent bodies (and instructions.md) via HTML comment directives, removed from compiled output:
<!-- #if claude -->
- Repo: !`basename "$(git rev-parse --show-toplevel)"`
<!-- #else -->
- Run `git rev-parse --show-toplevel` to find the repo root.
<!-- #endif --><!-- #if codex --> works the same way, and blocks nest.
Codex fallbacks
Codex has no inline-shell preprocessing, argument substitution, or ${CLAUDE_*} expansion, so unguarded Claude-only features are rewritten automatically in the Codex output:
| Claude feature | Codex output |
| ------------------------------------------------- | ---------------------------------------------------------- |
| !`command` and ```! fences | Plain code spans plus a note telling the agent to run them |
| $ARGUMENTS, $0, $ARGUMENTS[1], named $foo | Prose ("the arguments the user provided", …) |
| ${CLAUDE_SKILL_DIR} / ${CLAUDE_PROJECT_DIR} | "this skill's directory" / "the project root" |
| ${CLAUDE_SESSION_ID} / ${CLAUDE_EFFORT} | Dropped — doctor warns; guard these with #if claude |
doctor flags every automatic rewrite so you can decide whether the translation is good enough or deserves an explicit #if block.
Releases
Tag-driven (v*.*.*): the release workflow publishes to npm with provenance and attaches standalone executables (built with bun build --compile) for linux-x64/arm64, darwin-x64/arm64, and windows-x64 to the GitHub release.
Development
bun run dev # watch mode
bun test # tests (100% coverage enforced for src/)
bun run validate # full gate: format, lint, typecheck, tests, build, package checksGit hooks (Lefthook) format and lint on commit and run the full validate gate on push; CI runs the same gate on every push and pull request. Published src/ code must stay Node-compatible — Bun-only APIs belong in scripts/ and tests.
