How to Extend SkillSpector with Custom Analyzers: A Complete Guide
To extend SkillSpector with custom analyzers, create a Python module in src/skillspector/nodes/analyzers/ that implements a node function accepting SkillspectorState and returning an AnalyzerNodeResponse, then register it via an import in src/skillspector/nodes/__init__.py.
SkillSpector is NVIDIA’s open-source skill analysis tool that uses a LangGraph workflow to inspect code for security issues, anti-patterns, and tool misuse. The analyzer architecture is designed for extensibility, allowing you to inject custom static analysis logic without modifying the core engine. This guide walks through the exact source code structure and implementation steps required to add your own analyzers to the pipeline.
Understanding the Core Architecture
SkillSpector’s analysis pipeline is built as a LangGraph workflow where each analyzer operates as an independent node. Nodes communicate through a strictly typed state object, making the system modular and testable.
The three critical components you must understand are:
SkillspectorState([src/skillspector/state.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/state.py)): ATypedDictcontaining the shared state—includingfile_cache,manifest, model configuration, and accumulated findings—that flows through the graph.AnalyzerNodeResponse([src/skillspector/state.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/state.py)): The required return type for every analyzer node, containing a single fieldfindings: list[Finding].Finding([src/skillspector/models.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/models.py)): A dataclass describing individual issues with fields forfile_path,location,severity,title,description, andremediation.
The graph orchestration in [src/skillspector/graph.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/graph.py) automatically discovers all modules in the analyzers package using pkgutil.iter_modules, so any new module you add becomes part of the execution pipeline without manual wiring.
The Analyzer Contract
Every analyzer must implement a single function with this exact signature:
def node(state: SkillspectorState) -> AnalyzerNodeResponse:
...
Key constraints:
- Read-only state access: Analyzers should only read from
state(e.g.,state["file_cache"]) and never perform side effects. - Pure return values: The function must return
{"findings": findings}wherefindingsis a list ofFindingobjects. - No I/O: The graph reducer handles persistence; your analyzer focuses purely on logic.
Built-in analyzers like [static_patterns_tool_misuse.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py) demonstrate this pattern by retrieving content from the state cache, running pattern checks, and returning standardized findings.
Step-by-Step Implementation
1. Create the Analyzer Module
Create a new Python file in src/skillspector/nodes/analyzers/, for example my_custom_analyzer.py.
Import the required types from the SkillSpector core:
from skillspector.state import AnalyzerNodeResponse, SkillspectorState
from skillspector.models import Finding, Severity, Location
2. Implement the Node Function
Implement the node function to extract data from state, perform analysis, and return findings:
def node(state: SkillspectorState) -> AnalyzerNodeResponse:
findings: list[Finding] = []
# Access the file cache containing all skill files
for file_path, content in state["file_cache"].items():
# Your custom analysis logic here
if "dangerous_pattern" in content:
findings.append(
Finding(
file_path=file_path,
location=Location(line=1, col=1, end_line=1, end_col=1),
severity=Severity.HIGH,
title="Dangerous Pattern Detected",
description="Found dangerous_pattern in file",
remediation="Remove dangerous_pattern",
)
)
return {"findings": findings}
The Finding dataclass requires:
file_path: String path to the affected filelocation:Locationobject with line/column coordinatesseverity:Severityenum value (LOW,MEDIUM,HIGH,CRITICAL)title: Short issue identifierdescription: Detailed explanationremediation: Fix suggestion
3. Register the Analyzer
Open src/skillspector/nodes/__init__.py and add an import for your new module:
from .analyzers import my_custom_analyzer # noqa: F401
The import side-effect ensures the module is loaded when SkillSpector initializes. The graph builder in graph.py uses pkgutil.iter_modules to discover all analyzers in the package, so no additional registration is required.
4. Adjust Execution Order (Optional)
If your analyzer must run before or after specific built-ins, modify src/skillspector/graph.py where the analyzer_nodes list is assembled. The pipeline executes nodes in the order they appear in this list, so you can insert your analyzer at the desired position.
Complete Working Example
Here is a minimal, runnable analyzer that flags Python files containing TODO comments:
# src/skillspector/nodes/analyzers/todo_detector.py
"""Detects TODO markers in Python files."""
from pathlib import Path
from skillspector.state import AnalyzerNodeResponse, SkillspectorState
from skillspector.models import Finding, Severity, Location
def _find_todos(content: str, path: str) -> list[Finding]:
findings: list[Finding] = []
for lineno, line in enumerate(content.splitlines(), start=1):
if "TODO" in line:
col = line.find("TODO") + 1
loc = Location(
line=lineno,
col=col,
end_line=lineno,
end_col=col + 4
)
findings.append(
Finding(
file_path=path,
location=loc,
severity=Severity.MEDIUM,
title="TODO marker detected",
description="TODO comments may indicate unfinished code.",
remediation="Remove or resolve the TODO before deployment.",
)
)
return findings
def node(state: SkillspectorState) -> AnalyzerNodeResponse:
findings: list[Finding] = []
skill_path = state.get("skill_path", ".")
for absolute_path, content in state["file_cache"].items():
relative_path = Path(absolute_path).relative_to(skill_path).as_posix()
for finding in _find_todos(content, relative_path):
findings.append(finding)
return {"findings": findings}
After adding from .analyzers import todo_detector to src/skillspector/nodes/__init__.py, this analyzer automatically runs on every skill inspection.
Testing Your Custom Analyzer
Validate your implementation using the existing test infrastructure:
pytest -k my_custom_analyzer
Create dedicated tests in tests/nodes/analyzers/ to assert specific behavior:
from skillspector.nodes.analyzers import my_custom_analyzer
from skillspector.state import SkillspectorState
def test_custom_analyzer_detects_issue():
state = SkillspectorState(
file_cache={"test.py": "x = 1 # TODO: fix this"},
skill_path="/tmp",
# ... other required state fields
)
response = my_custom_analyzer.node(state)
assert len(response["findings"]) == 1
assert response["findings"][0].title == "TODO marker detected"
Summary
- SkillSpector uses a LangGraph workflow where analyzers are pure functions receiving
SkillspectorStateand returningAnalyzerNodeResponse. - Create new analyzers as modules in
src/skillspector/nodes/analyzers/implementing thenode(state) -> {"findings": list[Finding]}contract. - Register analyzers by importing them in
src/skillspector/nodes/__init__.py; the graph auto-discovers them viapkgutil.iter_modules. - Use
Findingobjects fromsrc/skillspector/models.pyto report issues with standardized severity levels and locations. - Modify
src/skillspector/graph.pyonly if you need to control execution order relative to other analyzers.
Frequently Asked Questions
What data is available in the SkillspectorState?
The SkillspectorState dictionary contains file_cache (mapping of file paths to contents), manifest (skill metadata), model_config (LLM settings), and findings (accumulated results from previous analyzers). You can access these via standard dictionary notation, but treat them as read-only to avoid side effects in the graph execution.
Can my analyzer call external APIs or tools?
Yes, though it should remain synchronous since the LangGraph workflow expects the node function to return immediately. If you need to call external tools, invoke them within the node function and include the results in the Finding description or remediation fields. Ensure any external dependencies are handled gracefully to prevent pipeline failures.
How do I disable a built-in analyzer without deleting it?
Instead of modifying the source files, you can exclude analyzers by modifying the analyzer_nodes list assembly in src/skillspector/graph.py. Filter out specific modules by name before they are added to the graph workflow, or conditionally import them in src/skillspector/nodes/__init__.py based on configuration flags.
What is the difference between Severity levels?
The Severity enum in src/skillspector/models.py defines four levels: LOW (style suggestions), MEDIUM (potential issues), HIGH (likely bugs or security risks), and CRITICAL (definite problems requiring immediate attention). Choose levels based on the potential impact of the issue being detected, as this drives prioritization in the final report.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →