skillctl is a CLI that copies and synchronizes agent skills from a
global (canonical) store to user-defined targets (name + root directory).
- Unit of management: directory at
<root>/<skill_id>/... - Identity check: directory digest based on relative path + content
- Diff inspection: run an external diff tool
- Safety:
--dry-runonly (complete plan listing, zero file operations)
- Visualize diff states between global and target
- Converge from global to target (install/update/skip)
- Import from target to global when needed (default: add-only)
- Compare a diffing skill via diff tool
- Conflict resolution such as 3-way merge
- Automatic backups or interactive confirmations
- Remote fetch (e.g. direct install from GitHub)
- Watch/daemon/autosync
- global_root: canonical skills directory
- target: sync destination (user-defined name + root path)
- skill_id: directory name (e.g.
git-release). No separators,.., or absolute paths - digest: hash computed from relative paths + contents in a directory
- state: comparison result between global and target (missing/same/diff/extra)
global_root/<skill_id>/...skill_idmust be a normal directory (no symlinks)
targets[].root/<skill_id>/...targets[].root/<skill_id>must be a normal directory (no symlinks)
Codex/OpenCode-specific discovery paths are not assumed by this tool (targets are fully user-defined).
Priority order:
- Use
SKILLCTL_CONFIGif set - If
XDG_CONFIG_HOMEis set,${XDG_CONFIG_HOME}/skillctl/config.toml - Otherwise
~/.config/skillctl/config.toml
-
global_root: string -
targets: arrayname: string(unique)root: string
-
[hash]algo: "blake3" | "sha256"(default:blake3)ignore: string[](glob patterns, default: empty)
-
[diff]command: string[](argv form, default:git diff --no-index -- {left} {right})
- Expand
~and environment variables ($VAR/${VAR})
- If
SKILLCTL_LANGis set, chooseja/en - Otherwise check
LC_ALL/LC_MESSAGES/LANG - Unsupported values default to
ja
- Regular files under a skill directory
- Relative path (normalized per stabilization rules)
- Content (raw bytes)
- Symlinks are not supported (error)
- Metadata such as mtime, owner, permissions
- Directory enumeration order (order is normalized)
- Enumerate files after applying
ignore - Sort by relative path ascending
- Feed relative path + content into the hash (renames are diffs)
- Files matching
hash.ignoreglobs are excluded - Recommended defaults (example):
.git/**,**/.DS_Store,**/*.tmp
missing: exists in global, not in targetsame: exists in both, digest matchesdiff: exists in both, digest differsextra: exists only in target (not in global)
- Columns:
SKILL | STATE | GLOBAL_DIGEST | TARGET_DIGEST - Digest may be shortened (e.g. first 3 + last 3)
push and import must separate Plan (diff/ops) and Execute.
-
Input:
<skill_id>or--all,--target <name> -
Decisions:
missing→ installdiff→ updatesame→ skip
-
Update method (implementation requirement):
- Copy to a temp location, then replace (no partial state)
-
--dry-run:- List install/update/skip (+ prune if applicable)
- No file operations
- Include target-only skills (
extra) for removal - Default is not to prune (safer)
-
Input:
<skill_id>or--all,--from <name> -
Default behavior:
- Import only skills missing in global (install)
-
--overwrite:- Replace global if same-name exists (explicit only)
-
--dry-run:- List planned ops, no file operations
diff.commandis argv and must include both{left}and{right}at least once- If either placeholder is missing, return a config error (exit code 3)
skillctl diff <skill> --target <name>replaces placeholders and runs it- If either path is missing, return an error with next action guidance
- Diff exit codes: treat 0/1 as success, others as error
doctor --global | --target <name> | --all- Checks per skill directory:
SKILL.mdexists and is a regular file (not symlink)- No symlinks inside the skill directory
- No unsupported file types (only dirs/files)
- Output format (per root):
ok <skill>when no issuesissue <skill> <message>for each issue- Summary:
checked: <count> issues: <count>
- When
--allis specified, outputs a labeled section per target
targetslist --global | --target <name>status --target <name> | --alldoctor --global | --target <name> | --allpush [<skill>|--all] --target <name> [--dry-run] [--prune]import [<skill>|--all] --from <name> [--dry-run] [--overwrite]diff <skill> --target <name>
0: success2: invalid CLI arguments3: config errors (missing/invalid config, unknown target, etc.)4: execution errors (copy failure, diff launch failure, etc.)
statusoutputs all four states correctlypush --dry-runlists planned ops and makes zero file changes- After
push, target skills converge tosame importdefaults to add-only,--overwritereplacesdiffcan run the configured commanddoctoroutputsok/issuelines per skill and summarychecked/issuesper root- Language selection follows
SKILLCTL_LANG>LC_ALL>LC_MESSAGES>LANG, defaultja
status --format json- Digest cache (performance)
- Filters (e.g. diff-only view)