SkillSpector's analyzers — especially the LLM semantic ones — can produce findings that are correct in general but not actionable for your skills (framework/architectural patterns, first-party tooling conventions, accepted lab practices). A baseline lets you suppress those known findings so that:
- the risk score reflects only un-triaged issues,
- re-scans surface only new findings (incremental CI/CD), and
- every suppression carries an auditable reason.
Suppressed findings never count toward the risk score or active finding count.
They remain in SARIF marked with an external suppression for auditability. They
are shown in the terminal/Markdown report only when you pass --show-suppressed,
and are always listed (machine-readable) in the JSON report under suppressed /
suppressed_count.
Addresses issue #88.
# 1. Accept all current findings into a baseline (run once).
skillspector baseline ./my-skill/ -o .skillspector-baseline.yaml
# 2. Commit the baseline, then scan against it. Only NEW findings are reported.
skillspector scan ./my-skill/ --baseline .skillspector-baseline.yaml
# Review what was suppressed.
skillspector scan ./my-skill/ --baseline .skillspector-baseline.yaml --show-suppressed| Command / option | Description |
|---|---|
skillspector baseline <path> [-o FILE] [--no-llm] [--reason TEXT] |
Scan and write a baseline that fingerprint-suppresses every current finding. Default output: .skillspector-baseline.yaml. |
skillspector scan <path> --baseline FILE (-b) |
Suppress findings matching the baseline before scoring/reporting. |
skillspector scan <path> --baseline FILE --show-suppressed |
Also list the suppressed findings (they still don't affect the score). |
A missing, malformed, or unsupported baseline file exits with code 2.
When a selected baseline or baseline output is stored inside the scan target,
SkillSpector treats that exact file as an explicit scope exclusion. This
prevents sensitive rule text from creating a finding against itself or entering
regenerated fingerprints. Other baseline files and sibling YAML/JSON files
remain in normal scan scope unless they are selected with --baseline or -o.
YAML or JSON (the .json extension selects JSON output when generating). Two
complementary mechanisms:
version: 2
scanner_version: "X.Y.Z" # generated automatically; do not edit
rules: # human-authored, glob-based, drift-tolerant
- id: "SQP-1" # glob over the finding's rule id
reason: "Trigger-phrase breadth is a description nit, not a vuln"
- id: "SSD-2"
path: "example-skill/SKILL.md" # glob over the finding's file
message: "*example false-positive phrase*" # glob over its description or matched text
reason: "False positive: benign trigger phrase, not an instruction"
fingerprints: # machine-generated, exact
- hash: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
rule_id: "SDI-2" # informational (for humans reading the file)
file: "example-skill/SKILL.md"
reason: "Accepted — reads its own environment for context"A finding is suppressed when every field a rule specifies matches it; unspecified fields match anything. Use this for:
- Global pattern suppression —
id: "SQP-1"(orid: "SQP-*") drops a rule or rule family across all skills. - Skill/file-scoped suppression — add
path:(and optionallymessage:) to scope the suppression to a specific skill, file, or message.
Field reference:
| Field | Matches against | Notes |
|---|---|---|
id (or rule_id) |
Finding.rule_id |
glob |
path (or file) |
Finding.file |
glob; * crosses /, ** is an alias for * |
message |
Finding.message, plus the matched text shown as finding in reports |
glob, case-insensitive; wrap a keyword in * for substring |
reason |
— | required; recorded in reports and audits |
Glob matching uses Python's fnmatch,
so * matches across path separators (*SKILL.md matches a/b/SKILL.md).
Rules are drift-tolerant: they keep working after line numbers shift or
content is reworded.
Each entry is a full SHA-256 digest over canonical JSON that binds the finding
to the SkillSpector version, normalized component path, complete decoded text
presented to the scanner, and every risk/evidence field (including rule,
severity, confidence, location, matched text, context, intent, and tags).
Generated by skillspector baseline, it is intentionally exact:
editing the source or upgrading SkillSpector keeps the finding active until it
is reviewed and the baseline is regenerated.
Every v2 entry must be a mapping with a 64-hex-character sha256: hash and a
non-empty reason. rule_id and file are informational fields for reviewers.
If source content is unavailable or scanner_version does not match, exact
fingerprints fail closed and suppress nothing. Use rules only when you
intentionally want a reviewed suppression to survive source drift.
Version 1 fingerprints omitted the matched evidence and source content, so a
benign and malicious finding could share a fingerprint when rule, file, line,
and generic message were unchanged. They cannot be upgraded safely without a
new scan and human review. SkillSpector rejects version 1 files that contain
fingerprints; rerun skillspector baseline, re-triage every generated entry,
and commit the v2 file. Legacy files containing only explicit rules remain
loadable with a warning so reviewed policy suppressions are preserved. Do not
copy old hashes into the new file.
Recursive multi-skill scans do not accept one shared baseline because exact
fingerprints are scoped to each independently scanned skill. Run each sub-skill
with its own baseline. A single-skill scan still supports --recursive together
with --baseline.
Suppression is applied in the report node (skillspector/nodes/report.py),
the single place where findings are scored and formatted, so the CLI and any
future REST API behave identically. The CLI loads the baseline file into a
skillspector.suppression.Baseline and passes it via graph state
(state["baseline"], state["show_suppressed"]); the report node partitions
findings into kept vs. suppressed via
skillspector.suppression.partition_findings.
- Triage the first scan and generate exact v2 fingerprints for individually
accepted findings. Reserve drift-tolerant
rulesfor deliberate, tightly-scoped policy suppressions: source changes do not invalidate them, so a broad rule can hide newly malicious content. - Commit the baseline file to the repo.
- In CI, run
skillspector scan <path> --baseline <file>; the build fails (exit 1) only when a new finding pushes the risk score above threshold. - Periodically review with
--show-suppressedand prune stale entries.