Skip to content
Back to skills

Validate Skills

ASecurity

the shape every skill pair must hold: the doc, its sidecar, its frontmatter (saves report to .construct/)

  • 3 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
ai-agentsgobashgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 19, 2026

npx -y skills add MaisonDeVolonte/construct --skill validate-skills --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Validate Skills?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Validate Skills
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/maisondevolonte-validate-skills/badge)](https://www.skillsdirectory.com/skills/maisondevolonte-validate-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: validate-skills
license: MIT
compatibility: requires bash, jq, git
description: the shape every skill pair must hold: the doc, its sidecar, its frontmatter (saves report to .construct/)
argument-hint: "[--help] [--quick] [--strict] [--keep] <path> [--test]"
when_to_use: "Authoring or editing any SKILL.md or its sidecar, adding a skill to a plugin, or deciding whether a skill is a trigger or a spec. Also when a listing looks truncated or a skill fails to load."
metadata:
  artifact: .construct/maintainer/validate-skills/
---

# Instructions

## the pair
a skill is one folder, named for its trigger, holding exactly two files:

- `SKILL.md` carries the frontmatter and the numbered steps, and nothing above them
- `<name>.sh` carries the sidecar, and the wayfinding header for the whole pair
- the doc has no wayfinder of its own, since two headers that can disagree is worse than one
- frontmatter opens line 1 with `name` matching the folder, and a `description` a reader sees
- `metadata.kind` is declared rather than guessed, and decides the rest of the frontmatter
- `kind: trigger` acts on the repo, so it sets `disable-model-invocation: true`; prose is not a gate
- `kind: spec` describes a shape, so it stays auto-loading and never sets that flag
- every frontmatter rule above is judged by `.claude/skills/export-readme/export-readme.sh` at
  the readme source; this pair's own checks stop at the body, the sidecar, the pairing, and the index

## the listing budget
`description` and `when_to_use` are the whole listing; everything else loads only once a skill does:

- the harness truncates the two of them, combined, at 1536 characters, and says nothing when it does
- past the cap the tail is dropped, so a skill can lose the clause naming when it fires and still list
- put the use case first, since a truncated listing keeps its opening and loses its end
- `description` says what the skill does; `when_to_use` carries trigger phrases and example requests
- the body is charged only when the skill loads, so long reference prose belongs under the frontmatter
- longer still belongs in a sibling file the body names, which costs nothing until it is read
- `metadata` is free-form and the harness ignores it, so it never counts against the cap
- export-readme measures the cap at the readme source, where the text is edited, not here

## the telemetry block
a doc that runs a sidecar opens its body with one, in this shape and no other:

```text
## Telemetry
```!
.claude/skills/validate-skills/validate-skills.sh "$ARGUMENTS"
echo "sidecar exit: $?"
```
- it already ran, so there is no command to issue
- no argument scans `plugins/` whole; a path argument grades that pair alone
- `scanned: N pair(s)` → what the run actually covered, which is never the maintainer skills
- `probed: N free-text skill(s)` → the pairs run on an apostrophe goal, the one check that executes
- `secrets:` above 0 → STOP and ask before touching the match; a committed key is already leaked
- fail (`sidecar exit` > 0) → an ERROR landed, or a WARN did under `--strict`; quote the findings
  table verbatim inside a markdown code block, and fix nothing until the user has read it
- success (`sidecar exit` = 0) → report `errors`, `warnings` and `secrets`, then work the findings
  list top to bottom; `none — every machine-checkable rule holds` is the clean result
- `--quick` reports inline and writes nothing, `--strict` promotes warnings, `--keep` holds scratch
- IF `--quick`, STOP after the inline report; `audit_file: none` confirms it, and nothing is appended
```

- the heading opens straight onto the bang block, since a blank line there reads as a gap
- the bullets close it flush, and each names one branch the sidecar can actually report
- a sidecar call under any other heading is an ERROR; unlabelled telemetry is telemetry nobody reads
- numbered steps start AFTER the block, at 1, since reading the output is not a step
- a spec that runs nothing carries no telemetry section at all, and `logs` is the one example
- an auto-loading spec guards the call on `$ARGUMENTS`, since a bare load must run nothing

## the help section
every skill answers `--help` the same way, so the section is COPIED rather than written. it sits
directly above `## Output Style`, and this doc's own `## Help` below is the canonical text:

- the fence holds placeholders, never one skill's filled values, so all of them stay identical
- that is what makes this one comparison instead of a shape per skill; a drift is a bad paste
- `HELP_BLOCK` in the sidecar is the source, and the doc is compared to it whole
- every field prints on every run, and one with nothing to say prints `none`
- each value is copied from the source named beside it, so two runs of one skill agree
- the sidecar carries the matching guard, since the bang block runs before the doc is read
- a help invocation refused only in prose has already paid for the run it was refusing
- the guard prints `help: requested` and exits 0, so the doc branches on a line rather than silence
- that marker earns a telemetry bullet, since a bare exit 0 otherwise reads as a clean run
- a doc that runs no sidecar carries the section anyway, and `logs` is the one example

## the subagent style
the output style is opt-in and a subagent never inherits one, so the LAST body heading of every
doc is `## Subagent Style`, and it cats that plugin's compressed brief into the turn:

```text
awk 'NR>1 && /^---$/ {p=1; next} p' "${CLAUDE_PLUGIN_ROOT}/subagent-styles/operator.md"
```

- it closes the doc, so the task instructions lead and the format rule lands last
- a `## ` section after it is an ERROR, since that section would read as the conclusion instead
- the command is compared byte for byte; a paraphrase cats nothing and reports the same clean run
- `CLAUDE_PLUGIN_ROOT` resolves per plugin, so one identical line serves every skill in the tree
- the awk sheds frontmatter, which is config for the picker and instruction to nobody
- the block carries no numbered step, since step numbers climb once across a whole doc
- it carries no bullet either; the missing-file case is already an ERROR two layers up
- a plugin that ships skills ships `subagent-styles/operator.md`, or the block cats an empty file
- the copies are one readme section exported three ways, never three files edited three times

## the body
frontmatter and pairing can both be correct while the body is structurally broken, which is how a
pasted archive shape corrupted two docs and every gate still reported clean:

- the body opens on `# Instructions`, the one h1 above every `## ` section, so the doc has a seam
- that h1 is never the template boundary; counting it as one switches the step check off silently
- the first `# ` heading after it is the boundary: above it the doc instructs, below it templates
- step numbers above that boundary climb, each appearing once; a repeat is two blocks fighting
- a heading closing no backtick it opened ate the line it was meant to reference
- an archive shape names the kind its own template h1 declares, never a sibling's
- a `<kind>` or `<Kind>` placeholder means the generic shape was pasted and never written
- the kind is read from that h1 and never from a mention, since a findings example may name a sibling
- an audit directory is named for the SUBJECT audited, so `git` is owned by `/gitgud:audit`
- a verify list reads under `## Verify`; fenced, it renders as sample output a reader skips
- no doc is required to carry one, since most fold the same steps into their numbered procedure

## the two blocks
every sidecar prints the same pair, in the same order, so each trigger doc reads one contract.
the name in a block header is the INVOCATION, so the header and the `/` menu can never disagree:

```text
=== /plugin:skill telemetry ===
key: value

=== /plugin:skill handover ===
# a note explaining the block, or why it is empty
git command one
git command two
=====================
```

- `telemetry` is what was measured; the trigger doc branches on it
- `handover` is what the user runs; every line is runnable as written, notes stay commented
- `trigger` is the optional third block: what the trigger runs as a tool call, never a paste
- a sidecar that needs to mutate emits the command instead of running it, into either block
- close every trigger with ONE copy-paste bash block holding the handover, in that same order

## the exec bit
every sidecar and every hook is tracked `100755`, since git records the mode an install copies:

- a `chmod` that never reached the index fixes the authoring machine and nobody else's
- the failure lands on the user as `permission denied` from a path they did not write
- `inject-support.sh` shipped `100644` and died on the first outside install, which is why this is a gate
- the check reads `git ls-files -s`, never the disk, so a local bit nobody committed still fails
- an untracked file has no recorded mode yet, so it is skipped rather than guessed at
- hooks pair with no doc, so they are walked repo-wide once instead of per skill
- the repair is `git update-index --chmod=+x <path>`, which the finding prints in full
- `shared/*.sh` stays `100644` on purpose, since those are sourced and never exec'd

## the smoke case
below the help guard, every sidecar carries one `--test` case, which `/test-skills` is built to run:

- the case is one line: `case " $* " in *" --test "*) echo "test: ok"; exit 0;; esac`
- it sits above every preflight and every confirm gate, since a gated test is a test nobody runs
- it names no path and no invocation, so there is nothing in it a sibling's copy could get wrong
- what a sidecar declares about itself is graded by `/test-skills` from outside, never from within
- a sidecar with no case is a WARN here, since this pair grades shape rather than execution
- ci runs `/test-skills --strict`, so a new sidecar without the case fails the build outright
- that gate went strict once all 32 adopted the case and the warning count reached zero
- `.claude/skills/test-skills/SKILL.md` owns the case's shape; this pair only checks it is there

## the read-only contract
- a sidecar may fetch, since that moves only remote-tracking refs, and may call the github api
- a sidecar may NOT stash, switch, merge, push, reset, restore, clean, or delete a branch
- a sidecar's commands are not tool calls, so neither the deny floor nor the hook ever sees them
- that is the whole reason for the rule: a sidecar running them was a silent bypass of both gates
- where a trigger genuinely needs to mutate, the floor opens a narrow allow and the TRIGGER runs it,
  which keeps the command in front of the gate; the sidecar emits it into a `trigger` block instead
- a step earns that block only by ADDING safety: `@git-continue`'s sync is recoverable at every
  step, and `@git-fresh`'s backup is the one line that makes the rest of its handover survivable
- a step that SPENDS safety never earns it, however convenient — clean, reset and force-switch
  stay in the handover no matter how many times the same paste gets asked for
- `plugins/gitgud/shared/handover.sh` carries the shared preflights, queries, and block emitters
- default branch resolution goes through `git_default_branch`, since `symbolic-ref` is denied

## the artifact
append one entry to `[audit_file]`, in the shape under `## the entry` below:

- the heading reads `## Validate Audit #[next_audit]: [timestamp]`, both taken from the telemetry
- `state` is the scope and the counts, as hyphen bullets, one clause each
- `findings` is the ERROR and WARN table the sidecar printed, or one bullet saying none held
- a secret finding names its file and line and NOTHING else, since copying the match spreads it
- `telemetry` is the sidecar's whole output, fenced and unedited, pasted last
- CREATE the file first if it does not exist, with `# <audit_file>` as its only line
- the sidecar names the path and the count and writes nothing, so a ci run leaves no entry

## the entry
> the artifact this skill appends to; `/gitgud:audit` grades the shape on its next run

# .construct/maintainer/validate-skills/YYYY-MM-DD.md
one file per day, appended to by every run:

- an entry captures one run at a moment in time, so it is never edited after the fact
- lines are hyphen bullets holding a single clause, capped at 100 characters

## Validate Audit #1: YYYY-MM-DD HH:MM

### state
the template, the pairs scanned, the skills probed, the index, and the three counts

*example:*
> - template: .claude/skills/validate-skills/SKILL.md, index: README.md
> - scanned: 28 pair(s), 5 free-text skill(s) probed
> - counts: 0 error(s), 0 warning(s), 0 secret match(es)

### findings
the table the sidecar printed, or one bullet saying every machine-checkable rule holds

*example:*
> | ERROR | plugins/x/skills/y/y.sh:1 | exec_bit | 100644, repair: git update-index --chmod=+x |

### telemetry
the sidecar's whole output, fenced and unedited

## Verify
> not part of the trigger; these run after a doc or a sidecar changes

- RUN `.claude/skills/validate-skills/validate-skills.sh` after touching a trigger doc or its sidecar; pass a path to scope it
- RUN `.claude/skills/export-readme/export-readme.sh` after touching frontmatter or a preamble; the readme is their source
- CONFIRM a new sidecar or hook reads `100755` under `git ls-files -s <path>`, since the disk bit lies
- FIX every ERROR, since each one breaks a rule this spec states outright
- STOP on a `secret` finding and ask the user before truncating it; the key needs rotating first
- JUSTIFY or fix every WARN; the sidecar tolerates them, the next reader may not
- ANSWER the checklist it prints, since those rules are the ones no script can judge

## Help
> IF the invocation carries `--help` or `-h`, this section is the whole turn:

```text
SKILL: /plugin:name
DESCRIPTION: <the `description` frontmatter, verbatim>
POSTURE: <the readme index's keyword for this skill>
FLAGS:
- --flag: <what it changes, in the telemetry bullet's own words>
ARGUMENTS:
- <arg>: <what it names>
ARTIFACT: <the `metadata.artifact` path, or none>
OUTPUT: <what lands in the turn: an audit entry, a handover block, an inline report>
SPEC: <this doc's own path>
```

- every field prints, in this order; one with nothing to say prints `none`
- every value is COPIED from the source named beside it, never composed fresh
- ask what they are actually trying to do, and what they have already tried
- name the flag or the sibling skill that fits their answer, then STOP
- run no step, write no file, and never fall through to step 1

Files in this skill

  • SKILL.md14.3 KB
  • validate-skills.sh42.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…