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.
Python★ 22,395+214 that day#4 on trending
E32.2/100
Duplication69
4.7% in python, typescript, javascript, makefile, bash, dockerDead code15
20.5%Complexity32
82.8% The score is jscpd's own --health model: markup, data and text left out, tests kept. The numbers below are the trending scan's.
2.91%duplicated code · 2,539 of 87,188 lines
172clones in code
115code files of 446 scanned
4.46%duplicated over all files · 779 clones
Most complex files
- src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py2052 cx
- src/skillspector/nodes/analyzers/static_python_shell_truthiness.py833 cx
- src/skillspector/nodes/build_context.py756 cx
- src/skillspector/nodes/analyzers/static_runner.py745 cx
- src/skillspector/dependency_sources.py728 cx
- src/skillspector/nodes/analyzers/bundled_execution_surface.py696 cx
- src/skillspector/nodes/analyzers/static_patterns_supply_chain.py679 cx
- src/skillspector/artifacts.py668 cx
- tests/unit/test_patterns_new.py655 cx
- src/skillspector/cli.py631 cx
Dead code 20.48% unused · 49 findings
- src/skillspector/nodes/analyzers/static_patterns_tool_misuse.pyunused-file
- src/skillspector/nodes/analyzers/static_patterns_supply_chain.pyunused-file
- src/skillspector/nodes/analyzers/static_runner.pyunused-file
- src/skillspector/nodes/analyzers/behavioral_taint_tracking.pyunused-file
- src/skillspector/nodes/analyzers/static_python_shell_truthiness.pyunused-file
- src/skillspector/nodes/analyzers/bundled_execution_surface.pyunused-file
- src/skillspector/security_reconstruction.pyunused-file
- src/skillspector/nodes/analyzers/mcp_tool_poisoning.pyunused-file
- src/skillspector/nodes/analyzers/static_yara.pyunused-file
- src/skillspector/nodes/analyzers/static_patterns_privilege_escalation.pyunused-file
The code number leaves out tests, fixtures, docs, examples, data files, styles, translations and vendored code, and 1 generated file: src/skillspector/suppression.py.
Largest clones in the code
- 169 lines in 2 placespython790 tokenssrc/skillspector/nodes/analyzers/behavioral_ast.py:653–821src/skillspector/nodes/analyzers/behavioral_taint_tracking.py:2284–2452
653"""Parse Python files via AST and detect dangerous execution patterns.""" 654components: list[str] = state.get("components") or [] 655file_cache: dict[str, str] = state.get("local_file_cache") or state.get("file_cache") or {} 656raw_file_cache = state.get("raw_file_cache") 657source_classifications = ( 658 state.get("python_source_classifications") 659 if "python_source_classifications" in state 660 else None 661) 662source_classification_limitations = state.get("python_source_classification_limitations") or {}First 10 of 169 lines.
- 65 lines in 2 placespython354 tokenssrc/skillspector/nodes/analyzers/semantic_developer_intent.py:251–315src/skillspector/nodes/analyzers/semantic_quality_policy.py:238–302
251 base_prompt=prompt, 252 model=model, 253 node=ANALYZER_ID, 254 timeout=timeout, 255) 256batches = analyzer.get_batches(files, file_cache) 257results = run_async(analyzer.arun_batches(batches)) 258outcome = getattr(analyzer, "_last_batch_outcome", BatchExecutionResult(successful=results)) 259findings = analyzer.collect_findings(outcome.successful) 260events, status = ledger_events_for_batches(ANALYZER_ID, outcome)First 10 of 65 lines.
- 11 lines in 6 placespython62 tokenssrc/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py:514–524src/skillspector/nodes/analyzers/static_patterns_excessive_agency.py:434–444src/skillspector/nodes/analyzers/static_patterns_memory_poisoning.py:631–641src/skillspector/nodes/analyzers/static_patterns_memory_poisoning.py:658–668src/skillspector/nodes/analyzers/static_patterns_output_handling.py:679–689and 1 more
514 message="File System Enumeration", 515 severity=Severity.MEDIUM, 516 location=loc(line_num), 517 confidence=confidence, 518 tags=tag, 519 context=ctx(match.start()), 520 matched_text=match.group(0)[:200], 521 complete_match=match.group(0), 522 ) 523)First 10 of 11 lines.
7 more
- 10 lines in 6 placespython57 tokenssrc/skillspector/nodes/analyzers/static_patterns_agent_snooping.py:164–173src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py:532–541src/skillspector/nodes/analyzers/static_patterns_supply_chain.py:2312–2321src/skillspector/nodes/analyzers/static_patterns_supply_chain.py:2329–2338src/skillspector/nodes/analyzers/static_patterns_system_prompt_leakage.py:984–993and 1 more
- 10 lines in 6 placespython57 tokenssrc/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py:514–523src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py:549–558src/skillspector/nodes/analyzers/static_patterns_excessive_agency.py:505–514src/skillspector/nodes/analyzers/static_patterns_output_handling.py:700–709src/skillspector/nodes/analyzers/static_patterns_rogue_agent.py:356–365and 1 more
- 52 lines in 2 placespython253 tokens
- 37 lines in 2 placespython233 tokens
- 10 lines in 5 placespython55 tokenssrc/skillspector/nodes/analyzers/static_patterns_agent_snooping.py:142–151src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py:432–441src/skillspector/nodes/analyzers/static_patterns_excessive_agency.py:412–421src/skillspector/nodes/analyzers/static_patterns_memory_poisoning.py:612–621src/skillspector/nodes/analyzers/static_patterns_output_handling.py:622–631
- 26 lines in 2 placespython205 tokens
- 44 lines in 2 placespython189 tokens
Largest clones over all files, tests and data included
- 169 lines in 2 placespython790 tokenssrc/skillspector/nodes/analyzers/behavioral_ast.py:653–821src/skillspector/nodes/analyzers/behavioral_taint_tracking.py:2284–2452
653"""Parse Python files via AST and detect dangerous execution patterns.""" 654components: list[str] = state.get("components") or [] 655file_cache: dict[str, str] = state.get("local_file_cache") or state.get("file_cache") or {} 656raw_file_cache = state.get("raw_file_cache") 657source_classifications = ( 658 state.get("python_source_classifications") 659 if "python_source_classifications" in state 660 else None 661) 662source_classification_limitations = state.get("python_source_classification_limitations") or {}First 10 of 169 lines.
- 7 lines in 11 placespythontests52 tokenstests/nodes/analyzers/test_security_reconstruction.py:537–543tests/nodes/analyzers/test_security_reconstruction.py:2034–2040tests/nodes/analyzers/test_security_reconstruction.py:2054–2060tests/nodes/analyzers/test_security_reconstruction.py:2124–2130tests/nodes/analyzers/test_security_reconstruction.py:2310–2316and 6 more
537result = static_runner.run_static_patterns_with_ledger( 538 {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}, [tm_module] 539) 540 541event = result["inspection_ledger"][0] 542assert event["outcome"] is LedgerOutcome.PARTIAL 543assert event["reason_code"] is LedgerReason.OBFUSCATED_INSTRUCTION_TEXT2034result = static_runner.run_static_patterns_with_ledger( 2035 {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}, [tm_module] 2036) 2037 2038event = result["inspection_ledger"][0] 2039assert event["outcome"] is LedgerOutcome.PARTIAL 2040assert event["reason_code"] is LedgerReason.STATIC_PARSE_LIMIT - 12 lines in 8 placespythontests57 tokenstests/nodes/test_semantic_quality_policy.py:819–830tests/nodes/test_semantic_quality_policy.py:851–862tests/nodes/test_semantic_quality_policy.py:879–890tests/nodes/test_semantic_quality_policy.py:937–948tests/nodes/test_semantic_quality_policy.py:965–976and 3 more
819 pytest.skip("sqp1_vague_triggers fixture not present") 820 821file_cache = _build_file_cache(skill_dir) 822state: dict = {"file_cache": file_cache} 823 824from skillspector.llm_analyzer_base import LLMAnalyzerBase 825 826orig_init = LLMAnalyzerBase.__init__ 827 828def _patched_init(self_inner, *args, **kwargs):First 10 of 12 lines.
- 9 lines in 8 placespythontests55 tokens
- 65 lines in 2 placespython354 tokens
By format, code only
| Format | Files | Lines | Clones | Duplicated lines | Duplication |
|---|---|---|---|---|---|
| python | 112 | 86,259 | 172 | 2,539 | 2.94% |
| typescript | 3 | 929 | 0 | 0 | 0% |
By format, all files
| Format | Files | Lines | Clones | Duplicated lines | Duplication |
|---|---|---|---|---|---|
| python | 313 | 206,722 | 766 | 9,665 | 4.68% |
| typescript | 4 | 1,794 | 6 | 71 | 3.96% |
| markdown | 74 | 9,586 | 3 | 37 | 0.39% |
| yaml | 18 | 1,139 | 3 | 90 | 7.9% |
| json | 5 | 159 | 1 | 14 | 8.81% |
| text | 16 | 567 | 0 | 0 | 0% |
| javascript | 1 | 432 | 0 | 0 | 0% |
| bash | 9 | 380 | 0 | 0 | 0% |
| mermaid | 3 | 178 | 0 | 0 | 0% |
| makefile | 1 | 158 | 0 | 0 | 0% |
Trending appearances
| Day | Rank | Stars | Files | Lines | Clones | All files | Code | Health | Commit |
|---|---|---|---|---|---|---|---|---|---|
| Oct 27, 2026 | #4 | 22,395 (+214) | 446 | 221,258 | 779 | 4.46% | 2.91% | E 32.2 | d7338f5 |
Want this for your own project? Install jscpd and run jscpd ., or measure it the way the baselines were measured.