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.
Generation also exits with code 2 without creating or replacing the output when
scan execution fails, including failed semantic analysis or unreadable primary
content. Resolve the failure and rerun the scan before accepting its findings.
Use --no-llm when you intentionally want to accept only static findings.
A successful but incomplete scan can generate a baseline for its observed findings, with a warning on stderr. This does not accept uninspected content or clear coverage gaps: subsequent reports retain their incomplete status and recommendation even if every observed finding is suppressed.
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.
Use skillspector baseline ./my-skill/ -o - to emit JSON on stdout. This is
available on Windows too. Redirect to a new file outside the scan target,
then replace an existing baseline only after the command exits with code 0.
Redirecting directly over the old file truncates it before the scan starts,
even when scanning fails. On Windows PowerShell 5.1, use cmd /c for the
redirection or Out-File -Encoding utf8; its default > encoding is UTF-16LE,
which the baseline loader does not accept. Check $LASTEXITCODE before replacing
the old baseline. File output
requires POSIX no-follow directory operations and fails before analysis when
unsupported. New baselines use mode 0600; existing baselines retain access metadata
when safely replaced. Updates requiring in-place writes are refused. The parent
must be writable, symlinked parents and non-regular files are refused, and macOS
also requires readable ancestor directories.
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.
LLM and meta-analysis can change explanations, remediation, confidence, or other
evidence between scans of unchanged files. Those results remain active when
their exact fingerprints differ. For repeatable static acceptance, use
--no-llm for both baseline generation and subsequent scans.
Generation fingerprints each original finding before report compaction, including
repeated matches on different lines and identical matches in different files.
Only identical exact fingerprints share an entry. A baseline generated by an
affected version may omit these occurrences; regenerate it to include them.
If the complete baseline exceeds the loader's size or record limits, generation
fails before replacing the output file instead of writing a partial baseline.
After validation, generation writes a complete temporary file beside the
destination and atomically replaces the output. A failed write or replacement
preserves the existing baseline. Existing destinations must be writable regular
files; symlinks and special files are rejected. Replacement requires a writable
parent directory. On POSIX, new files grant access only to their owner; existing
ordinary permission bits and ownership are preserved, including group-write access.
Access ACLs are copied through validated descriptors before any content is written.
A non-owner update, or an update that cannot preserve the destination's group,
fails without changing the existing baseline. Use a new owner-managed output
file or --output - redirected to a new file in those cases. Do not redirect
over the shared baseline: that rewrites the inode in place and can change other
hard-linked files. Generation does not fall back to in-place
writes, which would lose the atomic replacement and hard-link safety guarantees.
All path operations use the same no-follow parent descriptor so a directory swap
cannot redirect the write.
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 reject both --baseline and --use-shipped-baseline
with exit code 2. Exact fingerprints are scoped to each independently scanned
skill, and recursive multi-skill scans do not apply per-child shipped baselines.
Scan each sub-skill separately with its own --baseline or with
--use-shipped-baseline. A single-skill scan still supports --recursive
together with either option.
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.