Skip to content

ci: ship hooks/, and check a description names the installed command - #99

Open
MajorLift wants to merge 2 commits into
mainfrom
jongsun/ci/ship-hooks-and-trigger-check
Open

ci: ship hooks/, and check a description names the installed command#99
MajorLift wants to merge 2 commits into
mainfrom
jongsun/ci/ship-hooks-and-trigger-check

Conversation

@MajorLift

@MajorLift MajorLift commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Two follow-ups to #47, which merged before these landed.

hooks/ never shipped

copy_bundle_dirs covered references scripts assets adapters workflows — not hooks. A skill shipping a hook delivered everything except the hook.

This is the fourth instance of one root cause: a directory that exists in source and is not in the copy list. The earlier three were evidence's hooks/, domain knowledge/, and workflows/ — the last of which was 14 files referenced 22 times and never delivered. The BUNDLE_DIRS drift test from #47 now covers hooks too, so a fifth cannot land silently.

Verified end to end: a fixture skill with hooks/gate.py now delivers it.

A description that names a command nobody has

The installer prefixes every emitted skill, so a description advertising /<name> names a command no operator exposes — the reader gets mms-<name>. Since the description is the discovery surface, a wrong trigger string is a selection failure rather than a typo.

The check requires the prefixed form.

Its first version was wrong, and the fix is the interesting part: it flagged @metamask/gator-cli, reading a scoped package name as a slash command. A negative lookbehind now excludes @scope/name and path-like forms, and that exact case is a test rather than a comment.

main reports 0 errors across 47 skills with the rule active, so this gates new breakage rather than landing red.

Test plan

  • yarn test — 64 pass / 0 fail across three files
  • node .github/scripts/lint-skill-entry.mjs — 0 errors, 47 skills
  • Fixture skill with hooks/gate.py → delivered to mms-h/hooks/gate.py
  • Bare /demo in a description → fails, naming both forms
  • Prefixed /mms-demo → passes
  • @metamask/gator-cli in a description → passes, not treated as a command

Note

Branched fresh from main rather than pushed onto #47's ref. That branch was deleted on merge, and my local copy predated the testing-domain restructure that merged alongside it — reusing it would have reverted that work.

`hooks/` was absent from the bundle list, so a skill shipping a hook delivered
everything except the hook. This is the fourth instance of one root cause — a
directory that exists in source and is not in `copy_bundle_dirs` — after
`pr-validate`'s hooks, domain `knowledge/`, and `workflows/`. The `BUNDLE_DIRS`
drift test covers it now, so a fifth cannot land silently.

Separately, the installer prefixes every emitted skill, so a description
advertising `/<name>` names a command no operator exposes. The description is
the discovery surface, which makes a wrong trigger string a selection failure
rather than a typo. The check requires the `mms-` form.

Its first version flagged `@metamask/gator-cli` — a scoped package, not a slash
command. A negative lookbehind now excludes `@scope/name` and path-like forms,
and that case is a test rather than a note.
Stacked on #99, which is what makes `hooks/` reach a consumer at all.
**Base is #99's branch, not `main`** — review that one first.

## The gap

Copying a hook does not activate it. Claude Code runs one only once it
is registered in `settings.json`, and the path to register is
**absolute** — different per machine, per consumer repo, and per `mms-`
prefixed skill directory. So the setup reference could only ever say:

```
python3 /absolute/path/to/evidence/hooks/pr-evidence-gate.py
```

and leave the reader to work out what that is. Meanwhile `evidence`
cites the hook twice in its body as its enforcement mechanism, so
someone installing it reasonably assumes the gate is live. It isn't.

## Two surfaces, one output

**At install** — when any installed skill ships a hook, `tools/install`
prints the registration with every path resolved against the actual
install:

```
Note: 1 skill(s) ship a hook. Copying the file does not activate it —
Claude Code runs a hook only once it is registered in settings.json.

Add this to ~/.claude/settings.json (or <target>/.claude/settings.json):

  {
    "hooks": {
      "PreToolUse": [
        {
          "matcher": "Bash",
          "hooks": [
            { "type": "command", "command": "python3 <target>/.claude/skills/mms-gatekeeper/hooks/evidence-gate.py" }
          ]
        }
      ]
    }
  }
```

**On demand** — `metamask-skills hooks [--target <path>]` prints the
same thing, for anyone who scrolled past it or is re-registering later.
Says *"No installed skill ships a hook"* rather than printing empty
JSON, and exits non-zero only when the target has no installed skills at
all.

## What it deliberately does not do

**Neither surface writes to `settings.json`.** Editing a user's operator
config is a materially larger permission than "copy files into the repo
you pointed me at", and it should be decided deliberately rather than
inherited as a side effect of shipping one hook. This is also the only
hook in the corpus — a sample of one is thin evidence for automating a
write to `$HOME`.

## Test plan

- [x] `yarn test` — 69 pass / 0 fail across three files
- [x] Fixture skill with `hooks/evidence-gate.py`: file delivered,
registration printed, path resolved
- [x] Printed registration **parses as JSON** — asserted by
`JSON.parse`, matcher and command checked
- [x] `metamask-skills hooks` emits the same registration
- [x] Target with skills but no hooks → message, exit 0
- [x] Target with no installed skills → warning, exit 1

## Note

An earlier version emitted a trailing comma and told the reader to
delete it. Handing someone JSON that doesn't parse is worse than handing
them none, so the entries are joined properly and the output is valid as
printed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant