Skills and Hooks¶
See also: Repositories (where skills come from), Projects (where skills get installed), and MCP Servers (a parallel subsystem that reuses the same guidance mechanism).
What is a skill?¶
A skill is a directory that contains a SKILL.md file plus any supporting
files. SKILL.md carries YAML frontmatter (name, optional version, optional
description, optional license, optional declared executables, optional
declared hook names) and a Markdown body for human documentation.
my-skill/
SKILL.md frontmatter + documentation
run.sh supporting file (example)
helpers/ supporting subdirectory (example)
GUIDE.md optional guidance for the installing agent (see below)
hooks/ reserved - hook files live here
HOOK.md
hook-file.sh
The hooks/ subdirectory is reserved: its contents belong to the skill's
hooks and are never included in the skill body. Running a skill is the
agent's job; SkillKeeper only installs files and edits config regions.
Skill groups¶
Skills may be organized in a one-level namespace (group):
my-group/
my-skill/
SKILL.md
Group depth is exactly one. Deeper nesting not declared in the repository config yields an unresolved-path warning, not a silently guessed install.
Skill resolution schemes¶
SkillKeeper discovers skills in a cloned repository working tree using one of three schemes, applied in this order:
Scheme 3 - repository config (authoritative)¶
If skillkeeper.repo.yaml exists at the repository root, it is authoritative
and all auto-detection is skipped. The file declares explicit skill paths and
optional metadata overrides:
version: 1
defaults:
group: optional-default-group
skills:
- path: relative/path/to/skill # directory containing SKILL.md
name: optional-name-override
group: optional-group-override
include: ["glob", ...] # optional allowlist of skill dirs
exclude: ["glob", ...] # optional denylist
A malformed or schema-invalid skillkeeper.repo.yaml raises a
RepoConfigError naming the first offending field; the caller decides how to
surface that (the desktop app skips the repository for that operation rather
than crash).
Scheme 1 - flat layout¶
<SKILL_NAME>/SKILL.md at the root of the repository. No group. Hooks live
under <SKILL_NAME>/hooks/.
Scheme 2 - grouped layout¶
<SKILL_GROUP>/<SKILL_NAME>/SKILL.md. Group depth is exactly one. Hooks live
under <SKILL_GROUP>/<SKILL_NAME>/hooks/.
Schemes 1 and 2 are auto-detected by scanning up to two directory levels deep
for SKILL.md files; anything nested deeper produces the unresolved-path
warning mentioned above instead of a guessed install.
The scan skips two families of directory outright, resolving nothing from them
and raising no warning: hidden directories (any name starting with .) and
dependency or build trees (node_modules, vendor, target, dist). Hidden
directories are where every agent keeps its installed skills
(.claude/skills/, .codex/skills/, .cursor/skills/, .opencode/skills/,
.github/copilot/skills/), so a repository that itself uses SkillKeeper holds
skills it consumes rather than publishes; treating those as candidates reported a
nesting warning for perfectly normal projects. An explicit path in
skillkeeper.repo.yaml still reaches a skipped directory, which is the way to
publish a skill from one deliberately.
All three schemes have worked examples in the
skillkeeper-test-repo
fixture repository: a flat skill at
minimal-skill/SKILL.md,
a grouped one at
tooling/lint-skill/SKILL.md,
an inert scheme-3 sample at
skillkeeper.repo.yaml.example,
and a too-deeply-nested skill at
deep-nesting/level-two/too-deep-skill/SKILL.md
that must not resolve.
What is a hook?¶
A hook is an optional unit inside a skill (hooks/HOOK.md plus files) that
mutates an agent's own configuration. Hooks are privileged: they are
never installed implicitly. Installing or updating a hook always requires a
separate, explicit confirmation distinct from installing the skill body.
In the CLI, use --allow-hooks:
skillkeeper skill install <id> --agent claude --allow-hooks
Without that flag, the skill body installs and hooks are skipped with a clear notice. The desktop app requires the same explicit, separate consent before applying a hook.
Hook apply strategies¶
Because agents store configuration in different file formats, hooks use one
of three strategies declared in HOOK.md:
delimited-text¶
For comment-capable files. SkillKeeper inserts an owned, delimited region using the appropriate comment token:
# >>> skillkeeper:hook group/name:hookName v1.0.0 >>>
... generated content ...
# <<< skillkeeper:hook group/name:hookName <<<
The delimiter line carries a stable delimiterId so the exact region can be
located and removed later even if surrounding content changed. Of the five
built-in agents, only OpenCode uses this strategy today (comment token #,
target opencode.json).
json-merge¶
For JSON config that has no comment syntax. SkillKeeper merges its entries
into the correct array (by a dotted keyPath, hooks by default) and tags
each inserted node with a reserved ownership marker:
{
"hooks": {
"PreToolUse": [
{
"_skillkeeper": { "id": "...", "label": "group/name:hookName" },
"matcher": "...",
"hooks": [...]
}
]
}
}
The _skillkeeper marker plays the same role as a delimiter comment: it
makes the managed entry visible in-file and lets uninstall remove exactly the
owned node by its markerId. Existing user-managed hook entries are
preserved, and key order in the rewritten JSON is always sorted for a stable
diff. Claude, Codex, Copilot, and Cursor all use this strategy.
file¶
Hook-owned standalone files, tracked as ManagedFile entries flagged as
hook-owned. Removed by path on uninstall, like any other managed file.
Both hook layouts are shown in the fixture repository:
json-hooks-skill/hooks/pre-tool-use/HOOK.md
is a named hook directory declaring json-merge with a keyPath, and
text-hook-skill/hooks/HOOK.md
is the single-hook layout declaring delimited-text.
Per-agent skills, hooks, and guidance¶
| agent | skills root (project) | skills root (global) | hook strategy -> target file | guidance file |
|---|---|---|---|---|
| claude | <project>/.claude/skills/ |
~/.claude/skills/ |
json-merge -> .claude/settings.json |
CLAUDE.md at the base, or .claude/CLAUDE.md if no top-level file exists |
| codex | <project>/.codex/skills/ |
~/.codex/skills/ |
json-merge -> .codex/settings.json |
AGENTS.md at the base |
| copilot | <project>/.github/copilot/skills/ |
~/.config/github-copilot/skills/ |
json-merge -> .github/copilot/hooks.json (project) / ~/.config/github-copilot/hooks.json (global) |
.github/copilot-instructions.md at the base |
| cursor | <project>/.cursor/skills/ |
~/.cursor/skills/ |
json-merge -> .cursor/settings.json |
.cursorrules at the base, or .cursor/rules/skillkeeper.mdc if no legacy file exists |
| opencode | <project>/.opencode/skills/ |
~/.config/opencode/skills/ |
delimited-text (#) -> <project>/.opencode/opencode.json (project) / ~/.config/opencode/opencode.json (global) |
AGENTS.md at the base |
"Base" is the project directory for project scope, or the user's home directory for global scope. Claude is the reference adapter (its path and hook logic is the model the other four follow); each of the other four adapters isolates its path and hook choices to its own module, so confirming an agent's real on-disk layout only ever changes that one module, never its callers.
Skill guidance (GUIDE.md / RULES.md)¶
A skill may optionally ship a GUIDE.md or RULES.md file containing
guidance for the agent that installs it. GUIDE.md takes precedence when
both files are present. If neither exists, no guidance is installed. The
fixture repository's
documented-skill
ships both files, so it installs only its GUIDE.md.
On install, the guide body is written as a marked block into each target agent's guidance file (from the table above). The block uses delimiters:
<!-- SKILLKEEPER_START: <remote>; <id> -->
... guide body ...
<!-- SKILLKEEPER_END: <remote>; <id> -->
Where <remote> is the skill's source repository remote URL and <id> is
the skill identifier (group/name, or just name for ungrouped skills).
This is the same block mechanism MCP presets use for their rules
field, keyed the same way but built from a different identity.
Update and uninstall behavior¶
When a skill is updated, its guidance block is replaced in place, preserving
its position in the guidance file. If a reinstall drops the skill's
GUIDE.md/RULES.md (the source no longer ships one), the now-stale block
is removed instead.
When a skill is uninstalled, its marked block is removed by its delimiters (identified by remote URL and skill ID), even if the source guide no longer exists in the skill.
When multiple agents share a guidance file, or multiple skills share it, a block is removed only when no remaining installed skill still claims it - removing one skill's block never disturbs another skill's block in the same file. If removing the file's only remaining block empties a file SkillKeeper created, the file itself is deleted rather than left behind as an empty file.
Content hashing¶
Every managed file is hashed with plain lowercase-hex SHA-256 (sha256). A
skill's content hash is a single SHA-256 over its sorted,
<skill-relative-path>\0<sha256> lines - stable regardless of install
location, and computed the same way whether the source is a working-tree
skill (resolvedContentHash), an installed manifest (manifestContentHash),
or a freshly re-hashed directory found on disk. The .skid.yml identity
file (below) is always excluded from this hash, by its basename, so writing
or rewriting it never changes the hash it records. This is the hash compared
for update detection (see Repositories)
and used during reconciliation (below).
The .skid.yml identity file¶
Every skill install writes a .skid.yml file at the root of its installed
directory - SkillKeeper's own authoritative record of where the skill came
from:
# SkillKeeper identity file. Generated on install; do not edit.
schema: 1
remote: git@github.com:acme/team-skills.git # omitted for local-path installs
name: my-skill
group: my-group # omitted when ungrouped
version: <content hash, sha256 hex>
Any .skid.yml present in the skill's source is dropped before its files
are copied; installSkill always writes its own copy afterward, so an
installed skill's identity file is never a stale copy carried over from the
repository. It is tracked as a normal ManagedFile in the InstallManifest
(so uninstall removes it and verify checks it), and it is excluded from the
skill's own content hash.
.skid.yml matters because it survives even when SkillKeeper's local state
store does not: if the skill directory is copied into a project via Git, or
the app's state file is lost, .skid.yml still identifies the skill's name,
group, and source remote, and records the content hash it was installed
with. Reconciliation (below) reads it back to re-identify a skill purely from
what is on disk.
Install, update, and uninstall¶
Skill body install¶
- Resolve the skill from the repository working tree.
- Ask the agent adapter for the destination root.
- Copy skill body files (excluding
hooks/and any source.skid.yml), applying+xto files declared executable in the manifest or matched by the configured executable globs, and hashing each copied file. - Compute the skill's content hash over those files (skill-relative paths,
.skid.ymlexcluded). - Write the skill's own
.skid.yml, using that content hash asversion. - Record everything - body files, the identity file, and (if applied) hook
edits - as an
InstallManifest.
Hook install (privileged)¶
Runs only when --allow-hooks is passed (CLI) or explicit desktop consent is
given. Applies the hook's edit strategy and records the result as a
ManagedHookEdit in the InstallManifest.
Uninstall¶
Uninstall is the precise reverse of install: every recorded ManagedFile is
removed (pruning now-empty ancestor directories up to the destination root),
and every recorded ManagedHookEdit is removed by its own kind - a delimited
region by delimiterId, a JSON node by markerId, a hook-owned file by
path. Nothing not recorded in the manifest is ever touched, so external
skills (below) and unrelated hook entries in the same file are untouched.
Reconciliation¶
A project's skill directories are ordinary files on disk, so they can drift
from SkillKeeper's own install records: a git pull can add or remove a
skill directory, files can be hand-edited, or a skill can be installed by
some other means entirely. Reconciliation re-derives the install list from
what is actually on disk, for every tracked project and every agent's skills
root:
- A directory counts as a skill when it carries a
SKILL.mdor a.skid.yml. Its files are re-hashed and its content hash recomputed. - Its
.skid.yml, if present, supplies the skill's name, group, and source remote; a directory with no.skid.ymlbut a prior manifest keeps that manifest's remembered remote. - The remote is matched against every tracked repository's URL after
normalizing both (dropping transport, user, port, a trailing
.git, and letter case) - so re-adding the same repository under a different clone-URL shape still re-adopts skills installed under its earlier remote. - A skill whose remote does not match any tracked repository keeps its previous repository id (the repository was removed, but its skills remain installed and working), or, the first time reconciliation sees the directory at all, is recorded with an empty-string repository id - an unmanaged skill: present in the project, safe to keep or remove, but never source of an "update available" badge because it has no tracked repository to compare against.
Projects whose folder does not currently exist are left completely untouched: reconciliation neither scans nor prunes their recorded installs, so a temporarily unreachable project (an unmounted drive, a folder mid-move) never loses its history. See Projects for when reconciliation runs.
Each AgentAdapter also implements a lower-level discoverInstalled, which
simply lists the skill-shaped directories under its skills root without any
of the identity or hash logic above; reconciliation is the higher-level
process the desktop app actually runs to build its adopted/unmanaged skill
view.
Verification and repair¶
verify recomputes hashes for every ManagedFile and ManagedHookEdit
region and compares them to the manifest, reporting per file: ok,
modified, missing, or extraneous (a file present in a managed
directory that is not recorded in the manifest).
repair reinstalls the affected skill (and, only if originally installed and
re-consented, its hooks) to restore the recorded state, then removes the
extraneous files, so the install verifies clean afterwards. Every deleted path
is reported. Verify is read-only; repair mutates and is always explicit.
Two things bound that deletion. A skill's installed directory is named after the
skill alone, so two skills with the same name from different groups or
repositories share one directory; files recorded by any other install under the
same root are protected. And a recorded path that could resolve outside the
destination root (a skill whose declared name is .., say) disables pruning for
that install rather than being followed.
Encapsulation¶
Skill or hook content that itself contains SkillKeeper delimiter comments or
a _skillkeeper marker has those tokens encapsulated on install so they
cannot be mistaken for managed regions. The encapsulation is reversed on read
for display.