Skip to content
Back to skills

Validate

CSecurity

Idempotent framework health check of the instance (Part 6). Reports findings, fixes nothing without approval.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 13, 2026
developmentgoshellbashnodeawsgit

Works with

  • claude code

Security analysis

C60/100
  • criticalAccesses sensitive system or user directories
  • highPerforms destructive filesystem operations

Pro shows the line behind each finding and how to fix it

Scanned October 6, 2026

npx -y skills add AlexCiortan/setlist --skill validate --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Validate?

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

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

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
description: Idempotent framework health check of the instance (Part 6). Reports findings, fixes nothing without approval.
---

You are the framework health check (Part 6 of the committed edition). This
command is a thin binding: the check list lives here, project facts come from
`.claude/sdd.json` (role paths, trunk, gate command), and on any conflict the
edition wins. Load the full Part when in doubt:
`bash "${CLAUDE_PLUGIN_ROOT}/scripts/part.sh" 6`

Run any time; idempotent. Report findings as a list ordered by severity, and
fix nothing without approval.

Open the report by stating the plugin version you are operating from and
whether it is the newest one available: run
`bash "${CLAUDE_PLUGIN_ROOT}/scripts/plugin-skew.sh"` and quote its verdict. A
session binds its plugin tree at session start, so a health check run from a
stale session is reporting on the wrong tree, and every check below inherits
that. SKEW is a finding in its own right, above all the others, and its fix is
to restart the session rather than to change anything in the repo.

**Structural by design.** This check verifies shape: files in place, sections
present, wiring intact, config coherent. It does not judge semantic consistency
(a plan that contradicts its spec, criteria that miss the goal); that is
Planner work at spec time. Domain checks an instance accumulates (a shellcheck
sweep, a stack-specific hygiene rule) live in an instance-owned skill under the
instance's own name, never in a copy of this command.

Checks:

1. `.claude/settings.json` parses as JSON and still carries the permission
   rules (deny on .env reads, and since 2.6.0 the one Bash spelling
   `Bash(cat .env*)` beside them; ask on push, hard reset, force push, rm -rf).
   The `_comment` key in the permissions block is the deny list's own note
   (a deny list is a spelling list) and is not a finding; a `//` comment
   anywhere in the file IS one, because the harness then drops every rule.
2. The four stamped session hooks are present in `.claude/hooks/` and wired in
   settings.json: scope-hook on Write|Edit|MultiEdit|NotebookEdit
   (PreToolUse), bypass-deny on Bash (PreToolUse, 2.8.0),
   regrounding-hook on SessionStart, stop-hook on Stop (2.6.0; no matcher).
   An instance stamped before 2.6.0 lacks stop-hook, and one stamped before
   2.8.0 lacks bypass-deny, until `/setlist:upgrade` delivers it and its
   settings entry is restored from the template. A
   disabled or missing hook is a finding,
   not an error: report it with the settings line that would re-enable it.
   The pre-1.0.3 matcher `Write|Edit` is a finding (NotebookEdit writes files
   past the trunk rule), as is any hook entry with no explicit `timeout` (a
   timed-out hook is a skipped gate); both are fixed by re-wiring from the
   current template.
3. `.claude/sdd.json` parses, names the src and tests role paths (`roles.src`,
   `roles.tests`, each a string or a list of strings) and the `trunk`, and
   carries a non-empty `gate_command` once `scaffolded` is true, and, when it
   carries a `gates` block (2.6.0, the three tiers), that block is an object
   of three strings `commit`, `close`, `push` with `close` and `push` non-empty
   once `scaffolded` is true (an empty tier there is the stamped default, not a
   declaration, and the git hook refuses every close on it; a block of any
   other shape is refused `SLH-GATES-SHAPE` by the hooks). An absent block is
   not a finding: it reads as the single `gate_command` at `close` and `push`,
   and `/setlist:upgrade`'s refresh writes it. A role path
   of `"."` is a finding: the scope hook ignores it by design (Part 6);
   recommend enumerating the real code paths as a list.
   A role path that does not exist, or that holds no tracked file (`git ls-files
   -- <path>` prints nothing), is a finding too: it governs no code while the
   instance reads armed (a retrofit stamped with the default `src` and `tests` on
   a project laid out otherwise). Name it with the retrofit skill's inventory
   scan beside it (the tracked source files by top-level directory, most first),
   which `refresh-instance.sh` prints in its report, and recommend the real paths.
4. Required files exist: CLAUDE.md, README.md, ROADMAP.md, RUNBOOK.md,
   DECISIONS.md, .gitignore, .env.example, specs/STATUS.md, specs/TEMPLATE.md,
   the steering docs, and exactly ONE committed edition file at the repo root:
   `setlist.md` (an instance not yet upgraded to the setlist cutover carries
   the edition file under its original pre-cutover name instead; still
   exactly one, and any name other than setlist.md is a finding recommending
   /setlist:upgrade).
5. specs/STATUS.md has its five bounded sections (Current state, Spec
   inventory, Open chores, Open questions for the Planner, Pointers) AND
   passes row discipline: inventory notes and chore archive lines are single
   lines, and no resolved item lingers under Open questions.
6. The L2 container view exists as a Mermaid block in the document that holds it:
   `steering/structure.md` for an ordinary application, or, where the instance
   records that its L2 lives in its own README (the project draws itself for
   readers who are not its maintainers), that file. Report the absence against
   the home the instance actually uses; do not report `structure.md` missing a
   diagram it was never meant to carry.
7. .gitignore does not exclude `.claude/`.
8. No [PHASE 2 SLOT: ...] markers remain anywhere in the instance (a leftover
   slot means tailored generation skipped a file).
9. DECISIONS.md has its index table, and no INFERRED row lingers past the flip
   ceremony (retrofits only).
10. No stale generated skills linger: `.claude/skills/checkpoint/` and
    `.claude/skills/validate/` were removed by the plugin-era migrations
    (their duties ship as /setlist:checkpoint and /setlist:validate);
    report any survivor with the removal step from the upgrade protocol.
11. Binding dependencies are installed: `jq` resolves on PATH (all four
    stamped hooks need it), and, when `.claude/skills/browser-qa/` exists,
    Playwright resolves (`npx --no-install playwright --version`, never a download) with its Chromium
    installed. Without jq the session hooks report their verdict and PERMIT
    (the scope hook says so in its own text, the bypass deny is silent; both
    are advisory since v1.7), and the re-grounding pointer says so at session
    start. The git hooks fail closed, so commits, merges and pushes are
    refused until it is installed. Report the exact install command for
    anything missing; install nothing yourself. Read `claude --version` too,
    and report it beside the floor the plugin's README names: read the floor
    from the "Requires Claude Code" sentence of `${CLAUDE_PLUGIN_ROOT}/README.md`
    rather than from memory, since that sentence is the one copy. Below the
    floor, name the update. Below 2.1.277, report that the re-grounding pointer
    may be partly dropped on a resumed, continued or cleared session (the
    harness fixed that in 2.1.277), and name the update. Both are reports, not
    findings.
12. The instance records which plugin stamped it: `.claude/sdd.json` carries
    `plugin.version`. An instance stamped before 1.0.2 records none, which is a
    finding recommending `/setlist:upgrade` (the refresh records it), not an
    error. When a version IS recorded and it is OLDER than the plugin this
    session runs, the instance is carrying stale enforcement files and the same
    recommendation applies. When it is NEWER, say so plainly and recommend
    nothing yet: the instance was refreshed by a newer plugin than this session
    holds, so this session is the stale party, and any refresh it ran would
    reinstall older hooks over newer ones.
13. The `release` block is COHERENT (Part 6, the release rail). An absent block
    is NOT a finding: it reads as `model: none`, which is the default and the
    right answer for an instance that ships nothing yet. What is checked is that
    a DECLARED model has the marker it claims:
    - `model` is one of `none`, `tags`, `version-file`. Any other value is a
      finding: the skills cannot act on a model they do not know, and guessing
      one would govern a release nobody described.
    - `tags` with no tag matching the declared pattern: report as INFORMATION
      before the first release, a finding after one. A project that declared the
      model and has not yet cut anything is in a normal state, and a check that
      cannot tell those apart trains people to ignore it.
    - `version-file` with no file at the declared path: a finding, always. That
      model's whole claim is that the file at that path identifies what users
      run, so an absent file makes invariant (b) unsatisfiable rather than
      merely unexercised. The asymmetry with `tags` is deliberate.
    - A marker fact that is present but EMPTY (an empty tag pattern, an empty
      path) is a finding rather than an absent marker: it is a declaration
      somebody started and did not finish.
14. Every PARKED row in the STATUS inventory states BOTH a reason and a revisit
    trigger in its note (Part 5). A PARKED row missing either is a finding: the
    state exists to record a decision, and a row with no trigger is an abandoned
    branch with a label on it, which is exactly what the state was introduced to
    stop being invisible. Report the row and what it is missing; do not invent
    the trigger.
15. If `.claude/sdd.json` declares `identity.user_email`, report it beside the
    machine's current `git config user.email` as INFORMATION when they match.
    When they DIFFER, say so plainly: this item is the only place the declared
    identity is compared at all (since 2.8.0 nothing warns at commit time), and
    NOTHING refuses a commit made under the wrong one: no git hook and not the trunk
    audit reads `user.email`. Say that plainly, because this declaration has no
    enforcing layer; learning that during a health check is cheaper than learning
    it mid-close. An absent key is not a finding; the check is opt-in and most
    projects will not want it.
16. The ACTIVE spec carries a `Spec-hash:` field (Part 6). Its ABSENCE is
    INFORMATION, not a finding: specs authored before edition v1.7 do not have
    one and acquire it on their next REVISED cycle. Do not recompute or write
    the value here; that is checkpoint's job at a lifecycle transition, and a
    health check that silently re-stamps a hash would erase the very drift the
    field exists to surface.
17. If `.claude/sdd.json` declares `"attestation": {"required": true}`, the
    ACTIVE spec has an approval attestation at `specs/attest/NNNN.json` with its
    `.sig` beside it (Part 6). **Presence is INFORMATION and absence is not a
    finding**, on the same rule item 16 states and for the same reason: a
    project that has not turned the feature on has nothing missing, and a spec
    approved before the project turned it on acquires one on its next lifecycle
    transition. **Never write or re-sign one here.** That is checkpoint's job at
    an approval, in a session where a human is present, and a health check that
    quietly produced an approval would be manufacturing the exact thing the
    mechanism exists to make somebody do deliberately.

18. **The git-hook boundary is LIVE** (Part 6). The guarantee is the push-time
    trunk audit, the per-merge hooks are its early warning, and all three parts
    below have to hold for either to run at all:
    - `.githooks/` exists and carries `pre-commit`, `pre-merge-commit`,
      `pre-push` and `setlist-hook-lib.sh`, and the three hooks are
      EXECUTABLE. Git skips a non-executable hook silently, so a mode bit is
      the difference between a boundary and a decoration.
    - `git config core.hooksPath` is `.githooks`. Without it git runs
      `.git/hooks` and every file above is inert.
    - `git config merge.ff` is `false`. Without it a fast-forward merge fires
      NO hook at all and walks unreviewed work onto the trunk.
    Any of the three missing is a FINDING, not information: unlike the release
    block or the identity key, this is not opt-in, and an instance that has lost
    it is running with the advisory layer only while the edition says otherwise.
    Both config settings live in `.git/config`, which is not cloned, so a fresh
    clone legitimately lacks them and the fix is to re-run the refresh.
    Report what is missing and the exact command that restores it.
    - **The trunk audit's declared baseline** (`audit.baseline` in
      `.claude/sdd.json`, edition v1.18). The audit walks from the commit that
      introduced `.claude/sdd.json` unless this key declares another, and for an
      instance whose hooks arrived LATER than its stamp the default is not
      merely imprecise: every merge in between was made when no
      `pre-merge-commit` existed to record a completion, so the audit refuses
      every push and calls those commits "made after this instance adopted the
      rules". Two directions, both REPORT-ONLY. **Ask the audit's own
      question, never re-derive it:** run
      `bash "${CLAUDE_PLUGIN_ROOT}/scripts/refresh-instance.sh" .` in REPORT
      mode (no `--apply`; it writes nothing) and quote its
      `.claude/sdd.json audit.baseline` line, which is computed by
      `slh_baseline_frame`, the same function the trunk audit runs (edition
      v1.19, spec 0167).
      - **Present**: the key is usable when it resolves to a commit here and is
        an ancestor of the recorded trunk; the audit walks from the oldest
        commit on the trunk's own first-parent line that contains it (the
        declared commit itself, or the merge that brought it in), and the line
        names that frame. It should equal what the refresh writes: the commit
        that added `.githooks/pre-push`, or, when the refresh is delivering the
        boundary now and no such commit exists yet, the trunk's tip at that
        moment. Any other usable value is a DECLARATION that differs from what
        the refresh would write: say so, name both commits and how many merges
        lie between them, and leave it alone. A value the audit cannot use at
        all (not a full 40-character lowercase-hex id, a present value that is
        not a string, unresolvable, or not an ancestor of the trunk) is the same
        report with the refusal the line names, because that instance is
        refused at every push until it is corrected.
      - **Absent** while `.githooks/pre-push` was added in a LATER commit than
        `.claude/sdd.json` (compare the two with
        `git log --root --diff-filter=A --format=%H -- <path> | tail -n1`):
        that is a FINDING. Name the refresh as the fix and the value it would
        write, because this is the instance that is refused at every push.
        Absent with the two arriving together is correct and not a finding: the
        default is the same commit.

19. **The status record versus the page** (Part 3, edition v1.12). In a
    structured instance (`.claude/status.json` present), cross-check the
    record against specs/STATUS.md: a spec whose record token and inventory
    row disagree, a chore done in one and open in the other. **Divergence is
    INFORMATION, never a finding that blocks**, matching item 16's posture and
    for the load-bearing reason: the machine acts on the record, the page is
    for people, and a blocking sync check would make the human page
    load-bearing again, which is the disease the record cures. Report each
    divergence with both values so the human can fix the page (or discover the
    hand edit). The record's ABSENCE is not information and not a finding:
    a legacy instance is a supported state. A record that is present and
    MALFORMED is a FINDING: every gate that reads it refuses, so the instance
    cannot close anything until it is fixed, and this check is the friendly
    version of the message the hooks will deliver anyway.
20. **Double declaration** (Part 6, edition v1.12): one file in two specs'
    `Owns:` sets, or in a spec's set and a chore's `files`. INFORMATION, never
    a finding: the audit checks coverage per closing spec, so a double
    declaration is legal and sometimes honest (a file that genuinely changed
    hands), and refusing it at declaration time would need a cross-spec read
    whose cost nobody has measured. The one overlap the audit DOES refuse is
    at close time, two specs in flight at once (`SLH-OWNS-OVERLAP`, plugin
    2.11.0): a second close whose branch never took the first's change. Report
    the file and both owners; the human decides whether it is a handoff or a
    lie.
21. **The two wiring files the refresh never replaces** (2.6.0, spec 0132, the
    owner's ruling 2 of 2026-09-07). `.github/workflows/setlist-forge-check.yml`
    and `.github/CODEOWNERS` are delivered by `/setlist:upgrade`'s refresh when
    ABSENT and LEFT AS IS when they differ from the plugin's templates, because
    both are the team's wiring (a runner label, a matrix; the `@OWNER` slot and
    the paths the team adds). A silent leave is refused the same way a silent
    replace is: report, BY FILE, which of the two differs from its template
    (`cmp -s` against `${CLAUDE_PLUGIN_ROOT}/templates/root/.github/workflows/
    setlist-forge-check.yml` and `${CLAUDE_PLUGIN_ROOT}/templates/root/github/
    CODEOWNERS.tmpl`). The ownership file differing is INFORMATION once its slot
    is filled (a standing `@OWNER` slot is check 8's finding), with one clause
    the refresh's report carries too: a file that does not name the
    fourth protected path `/.github/` (ratification amendment 5, 2026-09-07:
    the check's own workflow, the ownership file and the issue form) leaves
    the check's wiring an unreviewed change, so say so by path and recommend
    the line under the team's own owner; the refresh never adds it. The workflow
    differing is INFORMATION naming the file, and a FINDING only when its edited
    form no longer runs the stamped check (`.claude/hooks/forge-check.sh` absent
    from its command line), because then the required check named `setlist
    forge check` verifies nothing. Either file ABSENT is a finding recommending
    `/setlist:upgrade`, which delivers it. Replace neither yourself.

22. **The diagram reports** (projects with `docs/diagrams/`). Where the
    directory does not exist, report nothing: the project has not opted in, and
    check 6 above is the whole diagram check for it. Where it does exist, these
    are REPORTS and never findings that block anything, because each has an
    honest answer that is "not yet" and the close is where refusal belongs:
    - **Over twelve nodes.** Any diagram file whose Mermaid block draws more
      than twelve primary nodes. Name the file and the count, and recommend the
      split (`docs/diagrams/components/<parent>/<child>.md`). Twelve is a
      drawing bound, never a refusal. For a diagram drawn INLINE in a document
      that is not a diagram file there is no child to split into, so recommend
      cutting a node instead of recommending a split that cannot be made.
    - **Undiagrammed ownership.** Role paths appearing in closed specs' `Owns:`
      lines that no L3 diagram names as a node. Name the paths; they are the
      parts of the system no picture claims anything about.
    - **Stale nodes from earlier specs.** Node names that are path-shaped and do
      not resolve in the tree, whose `%% spec NNNN` is not the current work.
      Name the node, the file, and the spec that introduced it, with the two
      honest exits: redraw it in a close that names the file in the diagram
      field, or retire it with a note. The close refuses only the CLOSING
      spec's own stale nodes, so these are the ones that accumulate quietly.
    - **Generated edges no hand diagram draws**, where `diagram_command` is
      declared: edges present in `docs/diagrams/generated/` that appear in no
      hand-drawn file. Each is either a real dependency nobody drew or a
      generated artifact worth ignoring, and saying which is the reader's call.
    Report each as a list with its subject, the measured evidence, and the
    one-edit fix, which is the message shape the whole diagram half uses.
23. **What loads from outside the instance** (plugin 2.11.0). Name the skills and
    plugins this machine loads into every session beside the instance's own:
    the directories under `~/.claude/skills/` (the user's) and
    `~/.claude/skills/synced/<bucket>/` (synced from the account), the plugin
    keys in `~/.claude/plugins/installed_plugins.json`, and the directories
    under `~/.claude/plugins/synced/<bucket>/` (`$CLAUDE_CONFIG_DIR` in place of
    `~/.claude` where it is set). The refresh's report prints the same list
    (`refresh-instance.sh .`, under "outside this instance"). Names only: read
    no content, and say that no Setlist check compares these files, so a stale
    edition claim or a model binding living there is in no report. This is a
    report, not a finding.
24. **The model binding answers now** (Part 2's third fact under the bindings
    table; plugin 2.11.0). Where `.claude/settings.json` sets `"model":
    "opusplan"`, say first, before anything runs, that this check spends two
    short headless sessions of the user's quota (measured at about 0.21 USD for
    the pair on 2026-09-29), then run
    `bash "${CLAUDE_PLUGIN_ROOT}/scripts/model-probe.sh"` and quote its lines.
    It probes the plan phase and the execution phase of `opusplan` and prints
    the model each served. The bootstrap probe's recorded answer is a yes or a
    no and names no version; do not read it in place of this check. rc 1
    (`[MP-PLAN-OFF-TIER]` or `[MP-EXEC-OFF-TIER]`) is a finding: a phase was
    served by a model outside its tier's family, which a managed allowlist
    changed after the stamp (exact version matching, or a denied model) or a
    provider that does not serve the family can cause, since the harness
    substitutes a blocked model rather than refusing it; report the phase, the
    model and the script's sentence, and fix nothing. rc 2
    (`[MP-PROBE-UNAVAILABLE]`, `[MP-FAMILY-UNREADABLE]`) is a report with its
    reason, never a pass. A served version older than the one Part 2's table
    names, in the right family, is a line, not a finding: the table names the
    alias. Where the model line is absent or names something else, say the
    check did not run and why.

## Gotchas (field-observed)

- A finding is not always a framework defect. A missing SessionStart wiring
  turned out to be an uncommitted local edit, correctly identified by diffing
  settings.json against git history before reporting. Classify each finding
  as committed drift or local edit first; the recommended fix differs, and
  restoring still requires approval either way.

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…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.