# checks

This skill should be used when the user says "create a check", "write a check", "add a check", "apply checks", "run checks", "/checks", "vulnerability pattern", "detection check", "check for common bugs", "scan with checks", or wants to create, apply, or manage simple vulnerability pattern files that agents use to find flaws. Checks are the simplest unit of agentic vulnerability detection — markdown files describing what to look for and how to assess matches. This skill is NOT for general code review or ad-hoc vulnerability analysis.

- **Kind:** skill
- **Source:** https://github.com/JoranHonig/grimoire
- **Page:** https://forefy.com/skills/3c92affb-94fd-42f3-bf6c-e495a9eaec24
- **API (JSON + files):** https://forefy.com/api/asr/3c92affb-94fd-42f3-bf6c-e495a9eaec24

---

## SKILL.md

---
name: checks
description: >-
  This skill should be used when the user says "create a check", "write a check",
  "add a check", "apply checks", "run checks", "/checks", "vulnerability pattern",
  "detection check", "check for common bugs", "scan with checks", or wants to create,
  apply, or manage simple vulnerability pattern files that agents use to find flaws.
  Checks are the simplest unit of agentic vulnerability detection — markdown files
  describing what to look for and how to assess matches. This skill is NOT for
  general code review or ad-hoc vulnerability analysis.
user_invocable: true
---

# Checks

Create, apply, and manage vulnerability pattern checks — simple markdown files that describe
what to look for in a codebase and how to assess matches.

## Philosophy

**Many simple checks beat one complex check.** Checks are starting points based on common
mistakes and best practices, not comprehensive analyses. Each check hunts one pattern. Agent
attention is the scarcest resource — keep checks focused and short. When reasoning gets complex,
split into multiple checks. Consult `references/design-principles.md` for the full rationale.

## Workflow

When this skill is activated, create a todo list from the following steps. Mark each task
in_progress before starting it and completed when done.

```
- [ ] 1. Determine intent — create new check, apply existing checks, or manage collection
- [ ] 2. (Create) Identify the vulnerability pattern
- [ ] 3. (Create) Write the check file
- [ ] 4. (Create) Validate the check
- [ ] 5. (Apply) Select applicable checks
- [ ] 6. (Apply) Run checks against the codebase
- [ ] 7. Collect and present results
- [ ] 8. Suggest follow-ups
```

---

### 1. Determine Intent

Ask the user (or infer from context) which mode they need:

- **Create** (steps 2-4) — write a new check from a finding, domain knowledge, or research
- **Apply** (steps 5-7) — run existing checks against a codebase
- **Manage** — index the collection, validate files, reorganize. Use the scripts directly:
  - `bash skills/checks/scripts/index-checks.sh grimoire/spells/checks/` to list all checks
  - `bash skills/checks/scripts/validate-check.sh <file>` to validate a specific check

If unclear, ask the user. Then skip to the relevant step.

### 2. Identify the Vulnerability Pattern

Three sources for new checks:

**From a confirmed finding.** The user or an agent found a specific bug. Generalize the pattern:
what was the observable code pattern? What conditions made it exploitable? What would a grep
for similar instances look like?

**From domain knowledge.** The user knows a class of bugs to look for (e.g., "check for ERC-4626
vault issues"). Extract the specific patterns and assessment criteria.

**From external research.** Use the librarian agent to gather context on a
vulnerability class, then distill into grep-able patterns and assessment rules.

For any source, extract:
1. The observable code pattern (what to search for)
2. The conditions that make a match problematic vs benign
3. A reasonable default severity and confidence level

Consult `references/design-principles.md` to decide whether the pattern should be one check or
multiple. If the pattern involves multiple independent sub-patterns or the assessment requires
deep reasoning, split now rather than after writing.

Check in with the user before continuing.

### 3. Write the Check File

Create a file at `grimoire/spells/checks/<slug>.md` where `<slug>` is a lowercase, hyphenated
version of the check name.

Follow the format in `references/check-format.md`. The file has:

1. **Frontmatter** — `name`, `description`, `languages`, `severity-default`, `confidence`,
   `tools`. Optionally `tags`, `related-checks`, `attribution-name`, and `attribution-url`.
   Add `attribution-name` and `attribution-url` when the check is inspired by external
   research, a known registry (e.g., SWC), an audit report, or another person's work.
2. **Pattern section** — what to search for. Keep patterns grep-able where possible.
3. **Assessment section** — how to evaluate matches. Categories, severity adjustments, benign
   cases to dismiss.

Keep the body under ~30 lines. If it grows longer, split into multiple checks.

Consult `examples/` for worked examples at different complexity levels:
- `examples/debug-assertions.md` — simplest possible check (one pattern family)
- `examples/rounding-direction.md` — scanning check (identify pattern, defer assessment)
- `examples/rounding-inflation-attack.md` — assessment check (deep reasoning on known pattern)
- `examples/erc4626-vault.md` — checklist-style check (multiple items for one standard)
- `examples/unchecked-return-values.md` — cross-language check

If `grimoire/spells/checks/` does not exist, create it. This directory is part of the spellbook
structure created by [[summon]].

### 4. Validate the Check

Run the validation script:

```bash
bash skills/checks/scripts/validate-check.sh grimoire/spells/checks/<slug>.md
```

This verifies:
- All required frontmatter fields are present and non-empty
- Body content exists after the frontmatter
- Body length is within the recommended limit (warns if >30 lines)

Review the check against the simplicity principle: is this one pattern? Is the assessment
clear? Could an agent apply this in a single focused pass?

Check in with the user before continuing.

### 5. Select Applicable Checks

Run the indexing script to see available checks:

```bash
bash skills/checks/scripts/index-checks.sh grimoire/spells/checks/
```

This outputs a tab-separated list: name, description, languages, severity, confidence, filepath.

Filter by:
- **Language** — match the `languages` field against the target codebase
- **Tags** — if the user has a specific focus (e.g., "defi", "crypto", "error-handling")
- **User selection** — present the filtered list and let the user choose which to apply, or
  apply all matching checks

### 6. Run Checks Against the Codebase

For each selected check, spawn a subagent with:

1. The check file content as its instructions
2. The target codebase path
3. Only the tools listed in the check's `tools` field

Each subagent operates independently with minimal context — just the check and the code. This
isolation is critical for attention management. Do not bundle multiple checks into one agent.

Subagents report back for each match:
- File path and line number
- Which pattern matched
- Assessed severity (adjusted from default based on context)
- Confidence assessment
- Brief description of why this is or isn't a finding

Run subagents in parallel where possible.

### 7. Collect and Present Results

Aggregate results from all subagents. Present findings grouped by severity:

1. **Critical / High** — show first, with full context
2. **Medium** — show with summary
3. **Low / Informational** — list briefly

For each finding, include:
- Check name and file path
- Match location (file:line)
- Assessed severity and confidence
- Brief description

After presenting findings, suggest invoking the familiar agent to triage them. The familiar
independently verifies each finding and filters false positives before the user acts on them.

### 8. Suggest Follow-ups

Based on results, suggest:

- **[[write-poc]]** — for confirmed high-severity findings that need proof-of-concept
- **[[cartography]]** — for flows that surfaced during checking and are worth documenting
- **Create more checks** — if patterns suggest related issues not covered by existing checks
- **Scribe distill** — invoke `/scribe-distill` to encode validated patterns into detection
  modules for future audits

---

## Guidelines

- **One pattern per check file.** If a check covers multiple unrelated patterns, split it.
- **Subagents for application.** Never apply checks in the main context. Each check gets its
  own subagent with isolated context.
- **Checks are pointers, not analyses.** They describe where to look and how to assess, not
  what the vulnerability is in depth. Use the librarian agent for background.
- **When in doubt, split.** Two simple checks always beat one complex check.
- **Checks live in `grimoire/spells/checks/`.** This is part of the spellbook directory
  created by [[summon]].
- **Validate before committing.** Run the validation script on every new check.

## examples

```

```

## examples/debug-assertions.md

# Example Check: Debug Assertions

The simplest possible check — one pattern family, two assessment categories, clear severity
rules. This example comes directly from the specification.

## Check File

```markdown
---
name: debug assertions
description: Flags security critical debug assertions which should be regular assertions.
languages: rust
severity-default: low
confidence: medium
tools: [Grep, Read]
tags: [assertions, invariants, rust]
---

Look for these patterns:

- `debug_assert!(...)`
- `debug_assert_eq!(...)`
- `debug_assert_ne!(...)`

Not all debug_assert! usage is problematic. Assess whether usage falls in one of two categories:
1. Asserting a known and assumed invariant
2. Performing essential input / state validation

If (1) might be the case, adjust severity to informational.

If (2) seems to be the case, determine potential impact and adjust severity accordingly.
```

## Why This Check Works

- **Single pattern family.** All three macros are variants of the same concept.
- **Grep-able.** The patterns are literal strings an agent can search for directly.
- **Clear assessment.** Two categories with distinct severity outcomes — no ambiguity.
- **Short.** Under 15 lines body. The agent spends its attention on the codebase, not the check.
- **Minimal tools.** Only needs Grep to find matches and Read to assess surrounding context.

## examples/erc4626-vault.md

# Example Check: ERC-4626 Vault Compliance

A checklist-style check for a specific standard. Demonstrates how to cover multiple items
for one component type without violating the simplicity principle — each item is a quick
assessment, not deep reasoning.

## Check File

```markdown
---
name: erc4626 compliance
description: Checks ERC-4626 vault implementations for common deviations and security issues.
languages: solidity
severity-default: medium
confidence: medium
tools: [Grep, Read]
tags: [erc4626, vault, defi, standard]
attribution-name: EIP-4626 Tokenized Vault Standard
attribution-url: https://eips.ethereum.org/EIPS/eip-4626
---

Identify contracts implementing ERC-4626 (look for `deposit`, `mint`, `withdraw`, `redeem`,
`totalAssets`, `convertToShares`, `convertToAssets`).

For each vault implementation, check:

1. Does `maxDeposit` return 0 when deposits should be paused?
2. Does `maxWithdraw` account for available liquidity (not just user balance)?
3. Does `previewDeposit` match actual `deposit` behavior (no hidden fees)?
4. Does `previewRedeem` match actual `redeem` behavior?
5. Are `totalAssets` manipulable via direct token transfer?
6. Does the vault handle rebasing or fee-on-transfer tokens?

For each deviation found, report the specific function and expected vs actual behavior.
Adjust severity based on whether the deviation could lead to loss of funds.
```

## Why This Check Works

- **Checklist, not essay.** Each item is a yes/no question the agent can evaluate quickly.
  The check doesn't explain ERC-4626 in depth — that's what the Librarian is for.
- **Single component type.** All items apply to one kind of contract (ERC-4626 vault).
  The agent doesn't need to context-switch between different code patterns.
- **Borderline length.** At ~15 lines body, this is well within the 30-line limit. If more
  items were needed (e.g., covering ERC-4626 + ERC-20 interactions), it should be a separate
  check.

## examples/rounding-direction.md

# Example Check: Rounding Direction (1 of 2)

This example demonstrates check splitting. The rounding vulnerability class is split into
two independent checks, each with focused reasoning. This check handles identification;
`rounding-inflation-attack.md` handles assessment of one specific exploitation path.

## Check File

```markdown
---
name: rounding direction
description: Identifies rounding operations and determines whether rounding favors the protocol.
languages: solidity
severity-default: informational
confidence: high
tools: [Grep, Read]
tags: [rounding, math, defi]
related-checks: [rounding-inflation-attack]
---

Look for these patterns:

- Division operations followed by multiplication (precision loss)
- Usage of `mulDiv`, `mulDivUp`, `mulDivDown` or similar helpers
- Explicit rounding: `Math.ceil`, `Math.floor`, `roundUp`, `roundDown`
- Integer division where the remainder is discarded

For each match:
1. Determine the rounding direction (up or down)
2. Determine who benefits from the rounding (protocol or user)
3. If rounding favors the user over the protocol, adjust severity to low

Do NOT assess exploitability here. That is handled by related checks.
```

## Why This Check Is Split

The spec discusses rounding as a case where splitting is essential:

- **Identification is easy, assessment is hard.** Finding rounding operations is a grep-able
  task with high confidence. Assessing whether rounding is exploitable requires deep reasoning
  about vault mechanics, deposit flows, and economic incentives.
- **Most rounding is benign.** Even rounding in the wrong direction is usually harmless. An
  agent that tries to assess both identification and exploitation in one pass will either
  skip important assessment steps (attention exhaustion) or over-report false positives.
- **Severity differs.** This scanning check defaults to informational because mere rounding is
  not a finding. The assessment check (rounding-inflation-attack) defaults to high because if
  the conditions are met, the impact is significant.

## examples/rounding-inflation-attack.md

# Example Check: Rounding Inflation Attack (2 of 2)

This check assesses whether rounding in vault-like contracts could enable an inflation attack.
It pairs with `rounding-direction.md` — that check identifies rounding sites, this one evaluates
a specific exploitation path.

## Check File

```markdown
---
name: rounding inflation attack
description: Assesses whether rounding in share/asset calculations could enable a vault inflation attack.
languages: solidity
severity-default: high
confidence: low
tools: [Grep, Read]
tags: [rounding, inflation, vault, defi, erc4626]
related-checks: [rounding-direction]
---

Look for vault-like patterns where shares are minted or burned in exchange for assets:

- `deposit()` / `mint()` / `withdraw()` / `redeem()` functions
- Share-to-asset or asset-to-share conversion math
- `convertToShares`, `convertToAssets` or equivalent

For each match, assess:
1. Can an attacker manipulate total assets or total shares independently?
2. Is there a minimum deposit or minimum shares requirement?
3. Does the first depositor path have special handling?

If (1) yes and (2) no and (3) no — likely vulnerable. Keep severity at high.
If mitigations exist (minimum deposit, dead shares, virtual shares) — adjust severity to low.
```

## Why This Check Works

- **Focused assessment.** This check does one thing: evaluate inflation attack feasibility.
  It doesn't try to find all rounding issues or assess other exploitation paths.
- **Low confidence is honest.** Vault inflation attacks require deep contextual reasoning.
  The check is a starting point — findings need manual review or PoC validation.
- **Clear decision tree.** Three questions with a direct severity mapping. The agent doesn't
  need to reason about what to do next.
- **Paired with a scanning check.** rounding-direction identifies the sites; this check
  evaluates one specific risk. Other risks (amplification, precision loss in fees) could be
  additional checks in the same `related-checks` family.

## examples/unchecked-return-values.md

# Example Check: Unchecked Return Values

A cross-language check demonstrating how the same concept maps to different patterns in C
and Go. Note: if the languages had very different assessment criteria, this should be split
into per-language checks.

## Check File

```markdown
---
name: unchecked return values
description: Flags function calls where error return values are ignored.
languages: [c, go]
severity-default: low
confidence: high
tools: [Grep, Read]
tags: [error-handling, reliability]
---

**C patterns:**
- Function calls whose return value is cast to `(void)` or not assigned
- Specifically: `close(`, `fclose(`, `write(`, `read(`, `malloc(` without NULL check

**Go patterns:**
- Error values discarded with blank identifier: `_, _ = someFunc()`
- Function calls without error capture where the function signature returns `error`

Not all unchecked returns are security-relevant. Prioritize:
1. Memory allocation (malloc, calloc) — adjust severity to high if unchecked
2. File/socket operations (read, write, close) — adjust severity to medium
3. Cleanup operations (close, free) — adjust severity to informational
```

## Why This Check Works

- **Cross-language with shared assessment.** Both C and Go share the same severity categories
  (allocation > I/O > cleanup). The patterns differ but the assessment logic is the same, so
  one check is appropriate.
- **When to split this instead.** If C needed different severity rules than Go (e.g., C
  unchecked malloc is critical because of null dereference, but Go has different failure
  modes), these should be separate checks with separate severity defaults.
- **High confidence.** The patterns are concrete and grep-able. Most matches are real unchecked
  returns — the question is severity, not validity.

## references

```

```

## references/check-format.md

# Check File Format

This reference defines the format for check files stored in `grimoire/spells/checks/`.

## File Location

All check files live in `grimoire/spells/checks/` with a slugified filename derived from the
check name: `grimoire/spells/checks/debug-assertions.md`.

## Frontmatter

Every check file starts with YAML frontmatter:

```yaml
---
name: debug assertions
description: Flags security critical debug assertions which should be regular assertions.
languages: rust
severity-default: low
confidence: medium
tools: [Grep, Read]
tags: [assertions, invariants]
related-checks: [unchecked-panic-paths]
attribution-name: Rust Security Advisory
attribution-url: https://example.com/advisory/123
---
```

| Field             | Required | Description                                                        |
|-------------------|----------|--------------------------------------------------------------------|
| `name`            | yes      | Short check name. Used in the index and for display.               |
| `description`     | yes      | One-line description of what the check detects.                    |
| `languages`       | yes      | Target language(s). Single value or YAML list.                     |
| `severity-default`| yes      | Default severity: `critical`, `high`, `medium`, `low`, `informational`. |
| `confidence`      | yes      | Expected confidence the finding is valid: `high`, `medium`, `low`. |
| `tools`           | yes      | YAML list of tools the check needs (e.g., `Grep`, `Read`, `Bash`).|
| `tags`            | no       | Freeform categorization tags for filtering.                        |
| `related-checks`  | no       | List of related check slugs (without `.md`).                       |
| `attribution-name`| no       | Name of the person, org, or resource that inspired the check.      |
| `attribution-url` | no       | URL to the source material (must start with `http://` or `https://`).|

**Constraint:** `name` and `description` must each be a single line. The indexing script relies
on this for parsing.

### Severity Levels

| Level           | Meaning                                                     |
|-----------------|-------------------------------------------------------------|
| `critical`      | Direct path to funds loss, RCE, or full system compromise   |
| `high`          | Significant impact, exploitable with moderate effort         |
| `medium`        | Real impact but requires specific conditions or chaining     |
| `low`           | Minor impact or theoretical, worth documenting               |
| `informational` | Observation, best-practice deviation, no direct impact       |

Agents adjust `severity-default` based on context. The default is a starting point, not final.

### Confidence Levels

| Level    | Meaning                                                          |
|----------|------------------------------------------------------------------|
| `high`   | Pattern match is almost always a real issue (e.g., hardcoded key)|
| `medium` | Pattern match needs context assessment to confirm                |
| `low`    | Pattern is a starting point; many matches will be benign         |

### Tools

The `tools` field declares what the applying agent needs. Common values:

- `Grep` — search for patterns in code
- `Read` — read files to assess context around matches
- `Bash` — run shell commands (e.g., for tool-specific queries)

Agents should restrict themselves to the declared tools when applying the check. This keeps
context focused and prevents scope creep during application.

## Body Structure

The body follows the closing `---` of the frontmatter. It contains two logical sections:

### Patterns

What to look for. Should contain:

- **Concrete search patterns** — literal strings, function names, import statements that can be
  grepped for directly
- **Structural patterns** — code shapes that require reading and understanding context
  (e.g., "a function that takes user input and passes it to an eval-like sink")

Keep patterns grep-able where possible. The agent uses the tools in the `tools` field.

### Assessment

How to evaluate matches:

- **Categories** — classify matches into types (e.g., "asserting invariant" vs "validating input")
- **Severity adjustment** — when and how to change severity from the default
- **Benign cases** — what makes a match NOT a finding, so the agent can dismiss quickly

### Body Constraints

- **Maximum ~30 lines.** If longer, the check should be split. See
  `references/design-principles.md` for splitting criteria.
- **No extensive background.** Don't explain the vulnerability class in depth. The agent can use
  the Librarian for additional context if needed.
- **No code snippets** unless they are the exact pattern to search for.
- **Imperative voice.** "Look for...", "Check whether...", "If X, adjust severity to..."
- **Self-contained.** Each check must be understandable without reading other checks, even if
  `related-checks` are listed.

## Example

See `examples/debug-assertions.md` for a minimal check, and `examples/rounding-direction.md` +
`examples/rounding-inflation-attack.md` for a split check pair.

## references/design-principles.md

# Check Design Principles

This reference covers the philosophy behind checks: why simplicity matters, how to manage
agent attention, and when to split checks.

## The Simplicity Principle

It is enticing to produce super complex checks that find lots of cool bugs. Resist this.

It is much more worthwhile having many simple checks that filter out common mistakes and bugs.

- A check that reliably finds one bug class is worth more than one that unreliably finds five.
- Complex checks exhaust agent attention and produce uncertain results.
- Simple checks compose: run 20 simple checks in parallel, get 20 focused results.
- Each check is a starting point, not a comprehensive analysis. The agent applies the check,
  produces findings, and those findings get triaged and investigated separately.

## Attention Management

Agent context is finite and degrades with length. Every extra line in a check dilutes the
agent's focus on the actual codebase.

**Checks should be limited in context.** The agent reading a check should spend most of its
attention on the codebase, not on understanding the check instructions.

### Context Isolation

Each check runs in its own subagent with its own context window. The subagent receives the
check body and the codebase — nothing else. This isolation is intentional:

- Prevents cross-contamination between checks
- Keeps each agent focused on one pattern
- Allows parallel execution without interference

Checks cannot depend on each other. If check B needs results from check A, they should be
merged into one check or the dependency should be handled at the workflow level.

### The 30-Line Rule

If the body of a check exceeds ~30 lines, it almost certainly needs splitting. This is a
soft limit, not a hard rule — but exceeding it should trigger a review. Long checks indicate
either:

1. Multiple patterns bundled together (split by pattern)
2. Complex assessment logic (split identification from assessment)
3. Extensive background (move to Librarian, keep the check focused)

## When to Split

Determining when a check should be split is difficult. These signals help:

### Multiple Independent Patterns

If a check says "look for A" and also "look for B" where A and B are unrelated patterns,
split into two checks. Each agent should hunt one thing.

**Example:** A check that covers both "unchecked return values" and "missing null checks" should
be two checks — the patterns are different, the assessment is different, and the agent's search
strategy is different.

### Complex Assessment

If identifying the pattern is easy but assessing whether it is problematic requires deep
reasoning, separate identification from assessment.

**Example from the spec — rounding errors:**

- **Check 1 (rounding-direction):** Identify where rounding occurs and in which direction.
  Severity: informational. Confidence: high. This is a scanning check.
- **Check 2 (rounding-inflation-attack):** For vault-like contracts with share/asset math,
  assess whether rounding could enable a vault inflation attack. Severity: high. Confidence:
  low. This is an assessment check.

The reasoning for "is this rounding exploitable via inflation attack" is substantial. Bundling
it with basic rounding identification would dilute the agent's attention on both tasks.

### Multiple Languages

If a pattern manifests differently in different languages, create per-language checks. The
search patterns, assessment criteria, and severity defaults may all differ.

**Example:** "Unchecked return values" in C (ignored return from `malloc`) vs Go (discarded
`error` with `_`) are different checks with different patterns and different severity profiles.

### Body Length

If the body exceeds ~30 lines after writing, split. This is the simplest signal and serves
as a backstop for the other criteria.

## Starting Points, Not Conclusions

Checks are starting points based on common mistakes and best practices. They identify *where
to look*, not *what the conclusion is*.

- The agent applies the check and produces findings
- Findings get triaged (by the researcher, or by Familiar when implemented)
- Confirmed findings get investigated further (write-poc, cartography)
- Validated patterns get automated (by Scribe when implemented)

Checks that try to do deep analysis themselves will produce unreliable results. Keep the
check simple, let downstream workflows handle depth.

## Future Considerations

The spec notes that Grimoire might implement coordination between checks or hierarchical checks
for token efficiency. This is not currently implemented. For now:

- Checks are independent units
- No check assumes results from another check
- The `related-checks` frontmatter field provides a lightweight connection for human navigation
  but has no runtime effect

## scripts

```

```

## scripts/index-checks.sh

```bash
#!/usr/bin/env bash
# index-checks.sh — Index check files by reading YAML frontmatter.
# Outputs tab-separated: name\tdescription\tlanguages\tseverity\tconfidence\tattribution-name\tattribution-url\tfilepath
# Usage: index-checks.sh [directory]
# Default directory: grimoire/spells/checks/

set -euo pipefail

dir="${1:-grimoire/spells/checks/}"

# Ensure directory exists
if [ ! -d "$dir" ]; then
  exit 0
fi

for file in "$dir"/*.md; do
  # Handle case where glob matches nothing
  [ -e "$file" ] || continue

  # Skip _index.md if present
  basename=$(basename "$file")
  if [ "$basename" = "_index.md" ]; then
    continue
  fi

  name=""
  description=""
  languages=""
  severity=""
  confidence=""
  attribution_name=""
  attribution_url=""
  in_frontmatter=0

  while IFS= read -r line; do
    # Detect frontmatter boundaries
    if [ "$line" = "---" ]; then
      if [ "$in_frontmatter" -eq 0 ]; then
        in_frontmatter=1
        continue
      else
        # End of frontmatter
        break
      fi
    fi

    if [ "$in_frontmatter" -eq 1 ]; then
      case "$line" in
        name:*)
          name="${line#name:}"
          name="${name# }"
          name="${name#\"}"
          name="${name%\"}"
          name="${name#\'}"
          name="${name%\'}"
          ;;
        description:*)
          description="${line#description:}"
          description="${description# }"
          description="${description#\"}"
          description="${description%\"}"
          description="${description#\'}"
          description="${description%\'}"
          ;;
        languages:*)
          languages="${line#languages:}"
          languages="${languages# }"
          languages="${languages#\"}"
          languages="${languages%\"}"
          languages="${languages#\[}"
          languages="${languages%\]}"
          ;;
        severity-default:*)
          severity="${line#severity-default:}"
          severity="${severity# }"
          ;;
        confidence:*)
          confidence="${line#confidence:}"
          confidence="${confidence# }"
          ;;
        attribution-name:*)
          attribution_name="${line#attribution-name:}"
          attribution_name="${attribution_name# }"
          attribution_name="${attribution_name#\"}"
          attribution_name="${attribution_name%\"}"
          attribution_name="${attribution_name#\'}"
          attribution_name="${attribution_name%\'}"
          ;;
        attribution-url:*)
          attribution_url="${line#attribution-url:}"
          attribution_url="${attribution_url# }"
          attribution_url="${attribution_url#\"}"
          attribution_url="${attribution_url%\"}"
          attribution_url="${attribution_url#\'}"
          attribution_url="${attribution_url%\'}"
          ;;
      esac
    fi
  done < "$file"

  # Only output if we found both required display fields
  if [ -n "$name" ] && [ -n "$description" ]; then
    printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$name" "$description" "$languages" "$severity" "$confidence" "$attribution_name" "$attribution_url" "$file"
  fi
done
```

## scripts/validate-check.sh

```bash
#!/usr/bin/env bash
# validate-check.sh — Validate a check file has required frontmatter fields.
# Usage: validate-check.sh <check-file>
# Exits 0 if valid, 1 if invalid. Prints issues to stderr.

set -euo pipefail

file="${1:-}"
if [ -z "$file" ] || [ ! -f "$file" ]; then
  echo "Usage: validate-check.sh <check-file>" >&2
  exit 1
fi

errors=0
warnings=0

# Required frontmatter fields
name=""
description=""
languages=""
severity=""
confidence=""
tools=""
attribution_name=""
attribution_url=""
in_frontmatter=0
frontmatter_closed=0
body_lines=0

while IFS= read -r line; do
  if [ "$line" = "---" ]; then
    if [ "$in_frontmatter" -eq 0 ]; then
      in_frontmatter=1
      continue
    else
      frontmatter_closed=1
      continue
    fi
  fi

  if [ "$in_frontmatter" -eq 1 ] && [ "$frontmatter_closed" -eq 0 ]; then
    case "$line" in
      name:*) name="${line#name:}" ;;
      description:*) description="${line#description:}" ;;
      languages:*) languages="${line#languages:}" ;;
      severity-default:*) severity="${line#severity-default:}" ;;
      confidence:*) confidence="${line#confidence:}" ;;
      tools:*) tools="${line#tools:}" ;;
      attribution-name:*) attribution_name="${line#attribution-name:}" ;;
      attribution-url:*) attribution_url="${line#attribution-url:}" ;;
    esac
  fi

  if [ "$frontmatter_closed" -eq 1 ]; then
    # Count non-empty body lines
    trimmed="${line// /}"
    if [ -n "$trimmed" ]; then
      body_lines=$((body_lines + 1))
    fi
  fi
done < "$file"

# Check frontmatter was found and closed
if [ "$in_frontmatter" -eq 0 ]; then
  echo "ERROR: No frontmatter found (missing opening ---)" >&2
  errors=$((errors + 1))
fi
if [ "$frontmatter_closed" -eq 0 ] && [ "$in_frontmatter" -eq 1 ]; then
  echo "ERROR: Frontmatter not closed (missing closing ---)" >&2
  errors=$((errors + 1))
fi

# Check required fields
check_field() {
  local field_name="$1" field_value="$2"
  field_value="${field_value# }"
  if [ -z "$field_value" ]; then
    echo "ERROR: Missing required field: $field_name" >&2
    errors=$((errors + 1))
  fi
}

check_field "name" "$name"
check_field "description" "$description"
check_field "languages" "$languages"
check_field "severity-default" "$severity"
check_field "confidence" "$confidence"
check_field "tools" "$tools"

# Check body exists
if [ "$body_lines" -eq 0 ]; then
  echo "ERROR: No body content after frontmatter" >&2
  errors=$((errors + 1))
fi

# Warn on long body
if [ "$body_lines" -gt 30 ]; then
  echo "WARNING: Body has $body_lines non-empty lines (recommended max: 30). Consider splitting." >&2
  warnings=$((warnings + 1))
fi

# Warn on malformed attribution URL
attribution_url="${attribution_url# }"
if [ -n "$attribution_url" ]; then
  case "$attribution_url" in
    http://*|https://*)
      ;;
    *)
      echo "WARNING: attribution-url does not start with http:// or https://: $attribution_url" >&2
      warnings=$((warnings + 1))
      ;;
  esac
fi

# Summary
if [ "$errors" -gt 0 ]; then
  echo "FAIL: $errors error(s), $warnings warning(s)" >&2
  exit 1
else
  if [ "$warnings" -gt 0 ]; then
    echo "PASS with $warnings warning(s): $file" >&2
  else
    echo "PASS: $file" >&2
  fi
  exit 0
fi
```

