Skip to navigation

Tier 1: Static and Security Validation

View as Markdown

Run Tier 1 directly to check a skill’s structure, security, quality, and dependencies:

Run Tier 1
skillevaluator tier1 ./my-skill

Static checks need no model key. With a configured provider, this command also runs rubric judging, LLM security analysis, and finding verification. Without a provider, those LLM stages are explicitly reported as skipped. Use --no-llm for static checks only, or --llm to require a provider instead of allowing a keyless run. Full scanner coverage still requires the external tools listed in Installation.

The two-variable provider setup is enough to configure the LLM stages for NVIDIA Build or OpenAI. Calling skillevaluator tier1 without a path shows help; expert subcommands remain available below.

What Tier 1 checks

The direct tier1 PATH workflow runs Tier 1 only, as does tier1 validate PATH. The focused checks below are also available as skillevaluator tier1 <command>. Top-level validate is the combined pipeline.

CommandPurposeNeeds a key?
Top-level validateRun the selected Tier 1 checks, Tier 2 deduplication, and Tier 3 autopilot for skillsYes for the full pipeline; --tiers 1 selects keyless static checks
quality-checkScore skill quality across four weighted categoriesNo
rubric-evalRun the nine-criterion LLM-as-judge rubricYes
security-scanRun SkillSpector static scanning and optional LLM analysisNo (only with --llm/--llm-verify)
pii-scanDetect PII, credentials, secrets, and local identifiersNo (only with --llm-verify)
lint-scriptsRun advisory AST-based checks on Python scriptsNo

validate is the umbrella command: all three tiers run by default for skills, with autopilot creating one starter evaluation case if no source exists. Tier 1 checks always gate the exit code (non-zero on failure or incomplete required scanner evidence), Tier 2 dedup runs and gates by default, and Tier 3 is advisory by default. Use --tiers 1 for static checks only, --no-block-on-dedup to keep Tier 2 advisory, or --block-on-agent-eval to promote Tier 3 into the gate.

Run the combined or focused validation commands

Unlike direct tier1 PATH, validate includes Tier 2 and Tier 3 by default for skills. Its Tier 1 LLM checks still run only when requested.

1

Run the default suite

All three tiers, provider and runtime required
skillevaluator validate ./my-skill

By default you get a compact pipeline view in the terminal — a per-check ticker per tier — and the run writes HTML and JSON report files unless you pass -r explicitly, which is then honored exactly (including -r cli for terminal-only output). Add --verbose for the full per-check detail stream instead of the compact view. The full pipeline needs the provider and runtime setup; use --tiers 1 for static checks only. The exit code is 0 when every gate passes and 1 when one fails.

2

Run the complete offline gate

Install the scanners, then select only Tier 1 for a fully hermetic run:

Complete offline Tier 1
brew install gitleaks semgrep
uv tool install git+https://github.com/NVIDIA/SkillSpector.git@v2.13.3
skillevaluator validate ./my-skill --tiers 1

With skillevaluator[security], Semgrep, SkillSpector, and Gitleaks installed, every required scanner produces evidence and the run is deterministic — the same input yields the same exit code, with nothing leaving your machine. Run skillevaluator doctor --tier 1 first to confirm that each scanner resolves and reports a supported version.

3

Narrow to specific checks

--checks (alias --tier1-checks) takes a comma-separated subset when you want fast, targeted feedback:

Selective checks
skillevaluator validate ./my-skill --tiers 1 --checks schema,security

Only the selected Tier 1 checks run; the others are skipped entirely.

4

Add LLM analysis (optional)

With a provider configured, --llm (alias --tier1-llm; disable with --no-llm or --no-tier1-llm) adds LLM security analysis on top of the static findings, and --llm-verify runs a second pass that reviews each static finding in context:

LLM-backed validation
skillevaluator validate ./my-skill --tiers 1 --llm --llm-verify

Confirmed findings keep their original severity. High-confidence false positives stay visible but are downgraded to INFO and carry verification metadata explaining the review. Static scanning always runs first — if a requested LLM stage is unavailable or returns unusable evidence, the run does not silently turn green. security-scan and pii-scan accept --llm-verify too.

Direct tier1 PATH and tier1 validate PATH print the terminal report and write JSON and HTML reports by default. Use -r cli for terminal-only output.

Reports for any of these runs are selected with -r/--report (cli, json, html, markdown, sarif) and written to -o/--output-dir (default reports/). File naming, BENCHMARK.md, and the JSON contract are covered in Reports & Results.

Content types

validate auto-detects what you point it at. Use --type when the path is ambiguous:

Type--type valueAuto-detection signal
SkillskillSKILL.md in skills/ or team-skills/
Rulesrules.mdc files in team-rules/
Workflowsworkflowsworkflow-rules.mdc in a workflow directory
PluginpluginA bundle-reference agent_plugin.yaml/.yml manifest, or a contained .claude-plugin/plugin.json manifest

Plugins are validated against their public contract; the quality, lint, and version checks are skill-only and are skipped for plugins, rules, and workflows.

For collections, pass a folder containing skills under skills/ or team-skills/. Folder-level validation discovers each live SKILL.md below the target while excluding configured names such as evals, results, versions, .git, .venv, node_modules, and __pycache__.

Validate a skill or a collection
skillevaluator validate ./skills/my-skill --type skill
skillevaluator validate ./skills --type skill --continue-on-failure

Validation profiles and policies

The bundled external profile is the default public-publication policy. It requires a well-formed metadata.author value but does not restrict the email domain. Missing or malformed author attribution is HIGH severity. Blocked license findings are CRITICAL; missing or unknown licenses are reported for review.

Profile selection
skillevaluator validate ./my-skill # default = external
skillevaluator validate ./my-skill --profile external # explicit public profile
skillevaluator validate ./my-skill --external # alias for --profile external
skillevaluator validate ./my-skill --policy ./policy.yaml # custom overlay
SKILLEVALUATOR_PROFILE=external skillevaluator validate ./my-skill

Precedence, highest to lowest: --policy overlay, --profile flag, SKILLEVALUATOR_PROFILE environment variable, then the bundled external default.

A custom policy overlays the external profile. It can narrow the accepted author domain and change severities by CATEGORY.check_name or CATEGORY.*:

policy.yaml
profile: community-strict
identity:
author_email_regex: '<[^>]+@example\.org>'
severity_overrides:
SCHEMA.author_missing: critical
LICENSE.*: critical

Severity overrides change how findings gate a run; they do not change the quality score or waive its --min-score requirement. Even an override of QUALITY.*: info leaves a below-minimum quality score failing.

The active profile name is stamped into the HTML report header and BENCHMARK.md, and printed in the run banner under --verbose, so reviewers can see which gate was applied.

The checks

For validate, the default set is schema, version, security, pii, license, code-integrity, unicode, quality, and lint. The dependency check requires explicit selection with --checks (alias --tier1-checks) for validate; direct tier1 PATH includes it by default. Accepted check-name aliases include code/code-risk for code-integrity, scripts/script-lint for lint, dependencies/deps/dependency-audit for dependency, and licence/license-check for license.

Validates SKILL.md frontmatter and repository placement:

  • YAML frontmatter must parse and satisfy the skill schema.
  • Frontmatter keys, including keys nested under metadata, must be strings. An unquoted key that YAML reads as a number, boolean, null, or date (123, yes, null, 2026-01-01) is a HIGH finding that names the key in YAML form, such as Field 'metadata.true': Keys should be strings; quote the key ("yes":) to keep it. Rules, workflows, and plugin manifests report non-string keys as schema errors too.
  • name and description are required. Names are 1–64 characters; descriptions are 1–1024 characters.
  • Names use kebab-case, start with a letter, and cannot have trailing or consecutive hyphens. The directory name must match the frontmatter name. The reserved words anthropic and claude must not appear in a skill name.
  • alwaysApply and globs are forbidden skill fields. Optional fields include license, compatibility, metadata, and allowed-tools.
  • The standard hierarchy is skills/<skill-name>/ or team-skills/<team>/<skill-name>/. A nonstandard standalone location is reported as an advisory finding.
  • Recognized skill-root subdirectories are agents/, references/, scripts/, assets/, evals/, tests/, tools/, and config/; anything else gets a LOW advisory finding. Extend the set per repository with SKILLEVALUATOR_SCHEMA_ALLOWED_DIRS (comma-separated names are added to the defaults, never replace them).
  • SKILL.md should stay at or below 500 lines; larger files receive an advisory finding.
  • The default profile expects metadata.author in Name <email@host> form but does not require a particular email domain.
  • metadata.domain accepts one category string or a list of them, such as domain: [backend, ai-ml]. Values are trimmed and deduplicated in order; a blank or null scalar means no domain. A list with more than 16 entries, or a non-string, blank, or over-128-character entry, is a HIGH Field 'metadata.domain' finding. The 128-character limit also applies to a single string.

To fix common findings: rename the directory to match name, remove forbidden fields, and move the skill under skills/.

security-scan, and the security check inside validate, run SkillSpector. Static scanning covers common skill attack patterns such as prompt injection and data exfiltration; each finding carries the matched pattern, severity, and location.

Security scanning
skillevaluator security-scan ./my-skill
skillevaluator security-scan ./my-skill --llm
skillevaluator security-scan ./my-skill --llm --llm-verify

The LLM pass requires a configured provider. It enriches the static result but does not replace static findings or make missing scanner evidence pass.

SkillSpector’s text pattern analyzers cannot read images, so it reports the analysis of a skill that references a PNG or JPEG file as partial. SkillEvaluator accepts that gap with a warning instead of INCOMPLETE only when the report shows no other gap and no fatal scope exclusion, and each referenced image is a regular, single-link (not hard-linked), non-executable file whose bytes have the chunk structure of a PNG or the marker structure of a JPEG and that is not reached through a symlink. The gap is accepted for at most 256 images and 128 MiB of image data in total. An image is never accepted inside a hooks, scripts, tools, agents, commands, prompts, rules, team-rules or workflows directory at any depth, or under a top-level config, skills, .mcp, .agents, .claude, .codex, .continue, .cursor, .gemini or .windsurf directory (names compared without regard to case). The structure check corroborates the file type; it does not decode the image or prove that it renders. PNG IDAT compressed data and ancillary chunks, and JPEG entropy-coded data and APPn or COM segments, are not inspected and can still carry other bytes. The image content itself is not assessed. Any other partial reason still leaves the scan INCOMPLETE. SkillSpector also reports a High AE1 (incomplete referenced artifact analysis) finding for such an image; from v2.12.0 it omits that finding when the skill renders a PNG with Markdown image syntax, such as ![Logo](assets/logo.png). An AE1 finding fails the check like any other High finding.

To explain why two otherwise identical --llm scans differ, the reports record the LLM provenance that SkillSpector v2.12.0 and later return. The Security result in the JSON report carries:

  • skillspector_llm_provenance: SkillSpector’s provider adapter, the model and SkillSpector revision behind each LLM analyzer, the requested and forwarded temperature, seed, and reasoning-effort controls, and a determinism classification. Provider output is never guaranteed to be identical.
  • skillspector_provider_id: the SkillEvaluator provider the scan was bridged from (nv_build, openai, openai-compatible, anthropic, or bedrock). nv_build and openai-compatible both run as SkillSpector’s openai adapter. The field is omitted when SKILLSPECTOR_PROVIDER selects a SkillSpector adapter directly (anthropic, bedrock, or openai) or names an unsupported provider. SKILLSPECTOR_PROVIDER=nv_build is bridged like SKILL_EVAL_LLM_PROVIDER=nv_build and recorded as nv_build.

The HTML report shows the same data in the Tier 1 LLM Provenance table on the Tier 1 tab. The record contains no prompts, endpoints, or credentials. A record that does not match SkillSpector’s provenance contract, or that contains a URL or credential-shaped value, is left out rather than partially shown. If the LLM stage fails or SkillSpector does not confirm LLM analysis, a valid record in SkillSpector’s output is still kept to show what was attempted, and the Security result stays incomplete. Provenance is recorded when you scan a single skill. A scan of a folder with several skills does not record it yet.

SkillSpector 2.11.1 and later tags an exact read-only /etc/passwd bind mount, such as docker run -v /etc/passwd:/etc/passwd:ro or the Podman, --volume, and quoted forms, as likely benign but still reports it as a HIGH Credential Access (PE3) finding. SkillEvaluator ignores that finding only when SkillSpector tagged it benign and every /etc/passwd on the reported source line is the source or target of such a mount. Writable or implicit-mode mounts, other source or target paths, and reads such as cat /etc/passwd still block.

SkillSpector 2.13.3 can report separate findings for the mount’s source and target; both must pass the same checks. Ignoring a mount never clears incomplete inspection, including refused or truncated LLM responses. A static report can say an LLM provider is available without having requested or applied LLM analysis; provider availability alone does not invalidate the static scan.

To accept reviewed findings, pass an audited baseline with --skillspector-baseline; see Audited SkillSpector baselines.

pii-scan, and the pii check inside validate, use configurable regex patterns to detect sensitive values. Instruction content in .mdc rules and workflow files is included, for both direct file inputs and directory scans.

GroupExamplesNotes
Personal informationPersonal paths, non-placeholder email addresses, phone numbers, SSNs, GPS coordinates, MAC addressesPersonal macOS/Windows paths are flagged; SSNs are CRITICAL
Credentials and secretsDatabase connection strings, hardcoded API keys, cloud keys, GitHub tokens, private keys, JWTs, webhook URLsMost credential findings are CRITICAL
Network and financialPublic IPs, credit-card numbers, cryptocurrency wallet addressesPrivate RFC1918 addresses are excluded

Common placeholders such as YOUR_API_KEY_HERE, example, test, dummy, and placeholder, along with @example.com and @test.com, are excluded, as are configured allowed paths.

A card-shaped number is reported only when it passes the Luhn checksum; all-zero numbers do not. Digits inside a canonical GUID/UUID, such as the COM interface ID {00000000-0000-0000-C000-000000000046}, are treated as an identifier unless a payment label (card_number, credit_card, cc_no, cc_number, pan, and similar) directly precedes the GUID on its line or ends the line before it. Every card-shaped number on a line is checked, so an identifier or non-card number earlier on the line does not hide a card after it.

The home-path check compares detected user paths against the submitter’s identity. When no identity is available, that check is skipped and logged — set SKILLEVALUATOR_SUBMITTER (or add author to SKILL.md) to enable it. SKILLEVALUATOR_SUBMITTER is combined with GITHUB_ACTOR, USER, LOGNAME, USERNAME, the OS login name, and the skill’s declared author — every identity that resolves is protected.

Looks for license evidence in frontmatter, common license files, and SPDX headers. Detected identifiers are normalized and matched against the packaged permissive allowlist and restrictive blocklist. Missing or unknown licenses are reported for review; blocked licenses fail according to the active profile (CRITICAL under the default external profile).

Frontmatter values and SPDX headers can be SPDX license expressions with AND, OR, WITH, and parentheses, such as MIT OR Apache-2.0. An expression passes only when every license in it is allowed, and any blocked license fails it. The +, -only, and -or-later forms of a blocked license, such as GPL-3.0+ or LGPL-2.1-only, and deprecated ids such as GPL-2.0-with-classpath-exception are blocked too. A WITH exception keeps an allowed license allowed only when it is in the packaged allowed_license_exceptions list (LLVM-exception, Swift-exception); other exceptions are reported for review. An expression that uses an upper-case AND, OR, or WITH but has unbalanced parentheses or a missing or repeated operator fails with an unsupported_license_expression finding. A value without those operators, such as MIT License or MIT License (MIT), is treated as a license name: it is matched by name as before or reported for review. A blocked license fails the value even when it does not parse, so GPL-3.0 (or later) and (GPL-3.0 OR MIT are blocked.

Combines three validation groups:

  • Code risk (Bandit and Semgrep) — SQL or command injection, hardcoded passwords, insecure cryptography, unsafe deserialization, shell=True, eval()/exec(), CWE patterns, and language-specific anti-patterns.
  • Secrets (Gitleaks) — API keys, tokens, passwords, private keys, database credentials, and other high-confidence secret patterns.
  • Hygiene — dead relative Markdown links, banned or unpinned dependency declarations, and static discovery of conventional Python test filenames.

Default Tier 1 does not import or execute target-controlled Python code. The test_discovery result is filename evidence only: it counts regular, in-tree, non-symlink test_*.py and *_test.py candidates without parsing or importing them. It reports execution_performed=false and coverage_measured=false; test success and code coverage must be established separately in a trusted project environment or an explicit sandbox.

A missing, timed-out, crashed, or malformed Bandit, Semgrep, Gitleaks, or SkillSpector run is treated as INCOMPLETE: terminal and saved reports are non-green and validate exits non-zero. See Scanner setup for installation.

Detects invisible Unicode characters associated with ASCII smuggling, hidden-data encoding, and trojan-source attacks.

CategoryRangeDefault severity
Unicode Tags (ASCII smuggling)U+E0000..U+E007FCRITICAL; decodes the hidden ASCII payload
BiDi overrides (trojan source)U+202A..U+202E, U+2066..U+2069, U+200E/F, U+061CHIGH
Zero-width charactersU+200B/C/D, U+034F, U+180E, U+2060, U+FEFFMEDIUM or LOW
Invisible operators and deprecated controlsU+2061..U+2064, U+206A..U+206FMEDIUM
Variation selectorsU+FE00..U+FE0F, U+E0100..U+E01EFLOW

Severity increases with suspicious run length. A BOM (U+FEFF) at byte zero is downgraded to INFO, and binary files are skipped using MIME and null-byte detection.

quality-check, and the quality check inside validate, produce a 0–100 composite score across four weighted categories.

CategoryWeightWhat it evaluates
Correctness35%Frontmatter, naming, examples, paths, and skill-type-specific structure
Discoverability25%Description quality, trigger wording, purpose, scope, and naming
Reliability25%Error handling, prerequisites, limitations, troubleshooting, and implementation safeguards
Efficiency15%Token budget, body length, repetition, instruction clarity, and reference use

The 5,000-token budget for SKILL.md is a recommendation, not a hard limit. A larger SKILL.md gets a MEDIUM quality_efficiency finding and a 15-point Efficiency deduction. The finding does not fail the check by itself, but the deduction can still take the score below --min-score.

Grades are A for scores of at least 90, B for at least 80, C for at least 70, D for at least 60, and F below 60. The default --min-score is 70. Passing requires the score to reach that minimum and no HIGH or CRITICAL quality findings under the active policy.

The minimum score is an independent gate. Policy severity overrides cannot waive it, and a collection average cannot hide an individual skill below the minimum. A below-minimum result includes a diagnostic such as Quality score 67.8/100 is below the required 70/100, retained in the JSON result’s gate_failures list. Improve the skill or explicitly choose the required threshold with --min-score.

Correctness reports XML-style tags in any frontmatter value as a HIGH finding (potential prompt injection). A string argument-hint, or an unquoted hint such as [<file>] that YAML reads as a list of strings, may use command-line placeholders such as <file> [--strict], <plan|apply|verify>, or <codec e.g. av1|h264>. A placeholder has at most four words, and | separates choices that are each one word or a quoted literal, after an optional label such as stage: or codec e.g.. Longer free text, shell command syntax (;, &, a backtick, $(, ${, or a pipeline such as <curl x | sh>), closing tags, attributes, self-closing tags, and role or control tags such as <system>, <tool_call>, or <script> are still reported there. A role word (system, developer, assistant, user, tool, context, instructions, prompt, override, rules, or policy) followed by prompt, message, call, use, result, or instruction counts as a role tag (<system_prompt>, <toolCall>, <user_message>), while other names such as <tool_name>, <user_id>, <prompt_file>, or <toolchain> are ordinary placeholders. A hint with a redirect, that is a > that closes no group (<cmd > file>, <cmd>file>, <cmd 2>file>, <curl x> /tmp/y>), or a group with anything after a nested placeholder other than its own closing > (<<curl x>/tmp/<y>>, <<x><y>>) or anything other than a plain name before it (<curl;sh<x>>, <$(curl x|sh)<x>>), is not masked at all, so it is still reported. A placeholder nested directly after a plain name, such as <R<NNN>>, is masked. For a list value, each item and the space-joined hint are checked separately. An arrow or comparison between groups, such as <old> -> <new>, <n> >= 1, or <count> (>0), is not a redirect. Every other field keeps the strict check.

The checker detects five skill shapes and applies relevant checks: script-based (Python or shell files under scripts/), lib-based (a Python module containing __init__.py), resource-based (assets, templates, design-system content, or resources), guide-only (documentation without executable or resource content), and hybrid (scripts plus library or resource content).

Quality signals use boundary-aware lexical matching for unambiguous terms and CommonMark parsing for structural link evidence. Links inside code, images, or comments do not count, and external URLs do not satisfy local-reference checks. Deterministic Tier 1 checks do not infer author intent from ambiguous prose such as MCP negation, temporal language, README guidance, or exclusivity claims. Use rubric-eval for semantic judgments about the documentation and Tier 3 for observed agent behavior.

If a quality score changes unexpectedly, inspect the finding message rather than adding keywords. The checker rewards usable instructions and evidence in their relevant context, not vocabulary alone.

Expected headings. Reliability deducts points when SKILL.md lacks an expected heading. The packaged quality-default.yaml expects ## Prerequisites (or ## Requirements), ## Limitations, and ## Troubleshooting (or ## Common Issues or ## FAQ), each worth 5 points. Pass --quality-config PATH to quality-check, tier1, or validate to use your own list instead. The file replaces the packaged defaults rather than merging with them, so copy any default entries you want to keep.

quality.yaml
expected_headers:
- heading: "## Prerequisites"
alternatives:
- "## Requirements"
points: 5
message: "No prerequisites/requirements documented"
suggestion: "Document dependencies, API keys, or setup needed"
- heading: "## Security"
points: 8
message: "No security notes documented"
suggestion: "Add '## Security' describing permissions and data access"
- heading: "## Examples"
Score with a custom heading list
skillevaluator quality-check ./my-skill --quality-config quality.yaml
skillevaluator tier1 ./my-skill --quality-config quality.yaml
skillevaluator validate ./my-skill --quality-config quality.yaml
FieldRequiredMeaning
headingYesA single ATX Markdown heading, such as ## Security
alternativesNoOther headings that satisfy the same entry
pointsNo (default 5)Non-negative number deducted from Reliability when no accepted heading is found
messageNoFinding message reported when the heading is missing. Defaults to Missing expected heading: '## Examples', listing any alternatives
suggestionNoFix suggestion attached to the finding. Defaults to Add a '## Examples' section, or Add one of these sections: ... when there are alternatives

The packaged defaults set message and suggestion explicitly, so their findings keep the original wording.

An entry is satisfied when the heading or any alternative appears anywhere in SKILL.md as text, so ## Security also matches ### Security Notes or a heading inside a code block.

The file is checked before any validation runs. A file that is missing, unreadable, not UTF-8, invalid YAML, has an unknown key (such as point instead of points), or lists the same heading twice is a usage error: the command exits with code 2 and names the file and the problem. validate checks the file even when --checks leaves out quality.

Example error
Error: Invalid value for '--quality-config': quality.yaml: unknown key(s) in expected_headers[0]: point (allowed: alternatives, heading, message, points, suggestion)

rubric-eval sends bounded skill documentation and selected supplementary content to your configured LLM provider. It scores nine criteria:

#CriterionImportance
1Description clarity and when to use the skillHigh
2Instruction clarity and actionable stepsHigh
3Example quality and query variationHigh
4Documentation completenessMedium
5Scope definitionMedium
6Professional tone and formattingLow
7Trigger simulation against positive and negative queriesHigh
8End-to-end workflow completenessHigh
9Actionable error handlingMedium

Each criterion is scored from 0 to 10. The local evaluator, not the model’s boolean, treats 7 or higher as passing. The overall score is an importance-weighted mean, and the rubric passes only when that score reaches --min-score and every criterion passes.

Rubric evaluation
skillevaluator rubric-eval ./my-skill --min-score 70

lint-scripts, and the lint check inside validate, parse Python scripts under scripts/ and report advisory findings. These findings do not make the Tier 1 gate fail.

CheckSeverityDetects
flat_scriptMEDIUMNo function definitions
deep_nestingMEDIUMControl-flow nesting deeper than 6
magic_numbersLOWRaw numeric constants outside the safe constant set
missing_shebangLOWNo leading shebang (#!)
no_input_validationLOWNo argparse, click, typer, or explicit raise pattern

The default version check validates an optional metadata.version as a three-part numeric value in major.minor.patch form:

SKILL.md frontmatter
metadata:
version: "1.2.3"

When --previous-version or SKILLEVALUATOR_PREVIOUS_VERSION is set, the current version must be present and numerically greater than the previous value. Without a previous-version bound, the version remains optional and the skill can rely on commit history. A previous-version bound is valid only when checking one skill; catalog validation must check each skill separately with its own bound.

Version check against a previous release
skillevaluator validate ./my-skill --previous-version 1.2.2

Scans requirements*.txt for known Python package vulnerabilities. For pyproject.toml, reads static PEP 621 dependencies and every optional group, then audits their exact version pins without installing the target or executing its build scripts. This covers direct declarations only; transitive dependencies require a resolved requirements file. Markers are evaluated against the scanner’s Python/platform and optional-group context; nonmatching declarations are reported as skipped. Version ranges, URL/path dependencies, dynamic or Poetry metadata, malformed declarations, and missing or failed scanner evidence leave the audit INCOMPLETE. Supply pinned requirements.txt for unresolved declarations. pip-audit is the primary scanner; Safety supplies secondary coverage for requirements files when installed separately. The audit may use network-backed vulnerability databases. JSON and HTML export data retain dependency_coverage per target, including requested and actually audited pins. Unreadable declarations have unknown declaration counts and an empty audited inventory, not a completed scan.

Opt-in dependency audit
skillevaluator validate ./my-skill --checks dependency

Scanner setup

Install skillevaluator[security] for the bundled Python scanners (Bandit and pip-audit). Install Semgrep and SkillSpector in separate tool environments, and install Gitleaks separately — it is a Go binary, not a pip package. See Installation for commands and the extras table.

Semgrep uses a policy packaged with the installed distribution, with metrics and version checks disabled; Tier 1 never fetches a registry policy at runtime. The other public Tier 1 configuration also ships inside the package under skillevaluator/config/: pii_patterns.yaml, unicode_smuggle_patterns.yaml, license_config.yaml, and profiles/external.yaml.

Bundled Python scanners resolve next to the SkillEvaluator interpreter before PATH; external Semgrep and SkillSpector installations resolve from PATH. Intentional replacements require an auditable absolute executable path in SKILLEVALUATOR_BANDIT_PATH, SKILLEVALUATOR_SEMGREP_PATH, or SKILLEVALUATOR_SKILLSPECTOR_PATH.

Scanner path overrides fail closed: a relative, missing, or non-executable override path fails the run rather than silently falling back. Required scanners that are missing or produce malformed output leave the run INCOMPLETE and non-green.

Audited SkillSpector baselines

Record reviewed SkillSpector exceptions in a SkillSpector YAML or JSON baseline and pass it with --skillspector-baseline to validate, tier1, or security-scan. SkillEvaluator never discovers a baseline on its own, including a .skillspector-baseline.yaml shipped inside the skill: applying exceptions is an operator or CI decision. A scan that reports suppressed findings without a requested baseline stays INCOMPLETE.

Apply a reviewed baseline
skillevaluator validate ./my-skill \
--skillspector-baseline ./trusted/skillspector-baseline.yaml

Keep the baseline outside the skill under review, for example in a checkout of the base branch or a separate policy repository. A baseline kept inside the skill is only as trusted as the review of that file: whoever edits the skill can edit its exceptions, SkillSpector leaves the selected baseline file out of its own scan so extra text in it is never inspected, and a version 2 rule needs only a non-empty reason. In a pull-request pipeline, the checked-out skill is the PR’s version, so pass a baseline from the target branch instead.

Prefer narrow rules. Every field a rule sets must match the finding. id (the rule ID), path (the file), and message (the finding text) are all case-insensitive globs, and reason is required.

config/skillspector-baseline.yaml
version: 2
rules:
- id: "PE5"
path: "SKILL.md"
message: "*--privilege[d]*"
reason: "Reviewed: the diagnostics container needs host device access."

The bracket class [d] matches the letter d, so the rule still matches --privileged, but the baseline file does not contain the literal text that triggers the finding. SkillEvaluator rejects a rule unless its id has at least two literal letters or digits in a row, or its path or message has at least four. Wildcards and multi-character classes break a run. This rejects id: "*" and also message: "*e*", which matches almost every finding because SkillSpector matches the glob case-insensitively against the finding text. Before the scan it also rejects a version 2 rule or fingerprint without a reason, rules or fingerprints that are not lists or contain entries that are not mappings, a file that is not valid YAML or JSON (including an impossible date such as 2026-06-31), and a file larger than 2 MiB or whose aliases and merge keys expand beyond SkillSpector’s own limits. An empty or comment-only file is an empty baseline, as in SkillSpector. SkillSpector checks the rest of the schema. If it rejects the baseline, it exits with code 2, the security check is INCOMPLETE, and the error suggests checking the file with skillspector scan <skill> --baseline <file>.

To waive individual findings exactly, generate fingerprints with SkillSpector. The command records every current finding, so review each entry before you commit the file:

Generate exact fingerprints
skillspector baseline ./my-skill --no-llm \
--reason "Reviewed: <rationale>" \
-o ./my-skill/config/skillspector-baseline.yaml

Fingerprints bind to the scanned file content and to the SkillSpector version that generated them. After a source edit, the affected findings are reported again. After a scanner upgrade, SkillSpector ignores every fingerprint, and SkillEvaluator reports fingerprint_version_mismatch with both versions: as an error when blocking findings remain, otherwise as a warning. Rules keep working across scanner versions.

Fingerprints only match static-only runs: tier1 --no-llm, or validate and security-scan without --llm. When the LLM security stage runs (tier1 by default when a provider is configured, or --llm), SkillSpector’s meta-analysis rewrites each finding’s message, explanation, remediation, confidence, or tags before the baseline is applied. Those fields are part of the fingerprint, so fingerprints from skillspector baseline --no-llm no longer match, and the LLM stage reports the findings again. SkillEvaluator then reports llm_stage_unmatched with both stages’ suppressed counts: as an error when blocking findings remain, otherwise as a warning. For runs with LLM analysis, use narrow id, path, and message rules instead; they still match because SkillSpector also checks message against the matched text.

A baseline stored inside the skill is excluded from the scan, including when SkillEvaluator scans a staged copy without evals/ outputs. For a folder of skills, the same baseline applies to each skill, and rule paths are relative to each skill.

For a single-skill target, when SkillSpector is installed, JSON reports record the outcome under results[].skillspector_baseline:

  • status: applied, no_matches, fingerprint_version_mismatch, llm_stage_unmatched, or not_applied.
  • file and digest: the baseline’s file name and a sha256: digest of its bytes. The local path is not recorded.
  • The baseline and runtime scanner versions, and the rule and fingerprint counts.
  • suppressed_findings from the static stage and, when the LLM stage completed, llm_stage_suppressed_findings; and the active and active blocking finding counts. All are taken from SkillSpector’s reports before any --llm-verify downgrade or policy severity override.
  • suppressed_by_severity and suppressed_rule_ids: the static stage’s suppressed findings by severity and their rule IDs, so CI can refuse a baseline that hides anything blocking. The Markdown and HTML line shows the severity counts.
  • message: the outcome or the reason the baseline was rejected.

Markdown and HTML reports show the same outcome as a skillspector_baseline line in the Security result, with the file name and digest, and a scan whose only findings were suppressed reads “No active security findings; N finding(s) suppressed by the requested baseline” rather than a clean result.

security-scan and tier1 security-scan on a folder of skills write one merged report without the JSON field. When a skill in the folder passes, its outcome is appended to that skill’s “All checks passed” line in the Markdown and HTML reports. validate on a folder validates each skill as its own job, so each skill’s JSON report records the field. The field keeps the fingerprint_version_mismatch and llm_stage_unmatched messages even when a later --llm-verify downgrade or policy severity override rebuilds the error and warning lists. See the SkillSpector suppression reference for the full baseline schema.

Exit behavior and scoring

Exit codeMeaning
0Success — every gate passed
1Validation failed — a check failed or required scanner evidence is incomplete
2Usage error — an unknown flag, a value outside an option’s type or choices, a target path that does not exist, or an invalid --quality-config file

Tier 1 checks always gate the exit code inside validate. Tier 2 gates when it runs unless --no-block-on-dedup is set. Tier 3 is advisory unless --block-on-agent-eval is set. The quality check requires the composite score to reach --min-score (default 70) as well as no HIGH or CRITICAL quality findings under the active policy. Severity overrides cannot waive the minimum. Reports record the effective gate membership for every tier so saved evidence and process status agree.

--fail-fast stops after the first failed check. -c/--continue-on-failure overrides it, records the full pipeline, and keeps scanning a collection past a CRITICAL finding. Wiring these exit codes into a merge gate — including a complete GitHub Actions recipe — is covered in Gate Your CI.

Troubleshooting

Install it with brew install gitleaks or download a binary from the official Gitleaks releases.

Install Bandit and pip-audit with skillevaluator[security]. Install Semgrep separately with brew install semgrep or uv tool install semgrep, and SkillSpector with uv tool install git+https://github.com/NVIDIA/SkillSpector.git@v2.13.3. Required scanner failures remain INCOMPLETE until the scanner is installed and returns valid evidence. skillevaluator doctor --tier 1 lists every scanner the selected checks need, with its resolved path and version.

SkillSpector releases before v2.13.1 can stop inspecting a valid Python or Perl helper early and record a static_parse_limit, obfuscated_instruction_text, or runtime_limit ledger exception. The security check is then INCOMPLETE, often with a High Incomplete referenced artifact analysis (AE1) finding at each line that names the helper. Check the installed release with skillspector --version and upgrade with uv tool install git+https://github.com/NVIDIA/SkillSpector.git@v2.13.3. If the exception remains after the upgrade, investigate it like any other incomplete scan.

Each SkillSpector run (the static stage, and the LLM stage with --llm) is stopped after 1,200 seconds by default, and the security scan is reported INCOMPLETE with skillspector timed out after 1200 seconds. SkillSpector also ends a scan at its own deadline (1,080 seconds by default in v2.13.3) and reports partial analysis, which is INCOMPLETE as well. Slow or local LLM providers can need more time. Raise both limits, keeping SkillEvaluator’s watchdog higher:

export SKILLSPECTOR_MAX_WORKFLOW_SECONDS=1800
export SKILLEVALUATOR_SKILLSPECTOR_TIMEOUT_SECONDS=2400
skillevaluator validate ./my-skill --tiers 1 --llm

Each stage gets the full watchdog, so a scan that stalls in both stages can take up to twice the value before it is reported. See Environment Variables.

Configure a supported provider or select a keyless Tier 1 run with skillevaluator tier1 ./my-skill --no-llm or skillevaluator validate ./my-skill --tiers 1 --no-llm (without --llm-verify). Direct Tier 1 automatically enables LLM stages when a provider is configured; --no-llm explicitly disables them. Top-level validate --no-dedup skips Tier 2 but still runs Tier 3 by default.

Confirm that the skill contains supported text or code extensions such as .py, .sh, .yaml, .yml, .json, .md, or .txt.

Use an existing, executable absolute path for the relevant SKILLEVALUATOR_*_PATH variable.

When SkillSpector reports partial analysis, the security scan is INCOMPLETE. The error quotes up to three of SkillSpector’s completeness exceptions as path [phase; reason_code; analyzers=...]. When there are more, it adds how many files each reason affected, for example and 463 more (incomplete files by reason: runtime_limit 462, obfuscated_instruction_text 4).

For runtime_limit, the error also says what to do, with one piece of advice for each kind of row the report contains:

  • Scan time budget. Rows that name regular analyzers, or no analyzer, mean SkillSpector ran out of time for the bundle: at its own whole-scan deadline (1,080 seconds by default in v2.13.3) or at a shorter stage budget such as bundle discovery. A skill that bundles a large corpus, such as vendored documents, generated figures or renderer scripts, hits the same limit on every rerun. Keep large reference material outside the skill and point to it. A plain rerun helps only when a few files are affected on a busy machine. If SkillEvaluator’s own SkillSpector timeout stops the run before SkillSpector’s deadline, the error is skillspector timed out after N seconds instead, with no list of files.
  • Sub-analysis budget. Analyzers named <analyzer>_runtime_limit, such as static_patterns_supply_chain_bytecode_runtime_limit, stopped at a time budget of their own, so bundle size does not matter. Releases before v2.13.0 charge the SC8 bytecode walk for time spent waiting behind other analyzers, so it can stop even on a skill with no bytecode. Rerun the scan, and update SkillSpector if the limit repeats.

Next steps