Security Scanner
Usage
/security-scanner # Scan all (plugins + skills)
/security-scanner --user # Scan user-level only (~/.claude/)
/security-scanner --project # Scan project-level only (.claude/)
/security-scanner --all # Scan ALL (ignore trusted sources and self-exclusion)
/security-scanner <url> # Scan from GitHub URL (public repos only)
/security-scanner --url <url> # Same as above (explicit form)
Scanning Process
Step 1: Load Settings
Search for security-scanner.local.md in the following locations:
- Project-level:
.claude/security-scanner.local.md - User-level:
~/.claude/security-scanner.local.md
Priority rules:
- If both files exist, use project-level settings only (project-level takes precedence)
- If only one file exists, use that file
- If neither file exists, proceed with default settings
From the selected file, extract:
report_languagefrom YAML frontmattertarget_agentslist from YAML frontmattertrusted_marketplaceslist from YAML frontmattertrusted_pluginslist from YAML frontmattertrusted_skillslist from YAML frontmatter
Default values (when not specified):
report_language:ja(Japanese)target_agents:["claude"]trusted_marketplaces:[]trusted_plugins:[]trusted_skills:[]
Validation:
report_language: Any string value accepted (AI will generate report in that language)target_agentsmust contain only valid agent IDs:claude,codex,gemini,agents- Invalid agent IDs are ignored with a warning
Error handling:
- If file exists but has invalid YAML syntax, warn the user and proceed with default settings (do not fail the scan)
Step 2: Determine Scope
Check arguments to determine what to scan:
Location filters:
- No location flag: Scan both user-level and project-level for all configured agents
--user: Scan only user-level paths for all agents intarget_agents--project: Scan only project-level paths for all agents intarget_agents
URL detection (highest priority):
- If
--url <url>is provided explicitly → Go to Step 2-URL - If any argument starts with
https://github.com/orhttp://github.com/→ Treat as URL, go to Step 2-URL - If any argument starts with
https://orhttp://but notgithub.com→ Error: "Unsupported host: {host}. Currently only github.com is supported."
Special modes (if no URL):
--all: Scan everything (skip Step 4 filtering entirely)
Step 2-URL: GitHub URL Scan
If URL is provided (via --url or auto-detected), follow this process instead of Steps 3-4.
Step 2-URL-1: Parse URL
Parse the GitHub URL to extract owner, repo, branch, path, and determine scan type:
URL Patterns:
- Directory:
https://github.com/{owner}/{repo}[/tree/{branch}/{path}] - Single file:
https://github.com/{owner}/{repo}/blob/{branch}/{path}.md
- Verify host is
github.com- If not: Error "Unsupported host: {host}. Currently only github.com is supported."
- Extract
ownerandrepofrom path segments - Determine scan type:
- If URL contains
/blob/and ends with.md→ Single file scan - Otherwise → Directory scan
- If URL contains
- For directory scan:
- If
/tree/{branch}/{path}exists, extractbranchandpath - If no
/tree/, setbranchto empty (use default) andpathto empty string
- If
- For single file scan:
- Extract
branchand file path after/blob/{branch}/
- Extract
Step 2-URL-2: Fetch Content
For Single File Scan:
- Convert
/blob/URL to raw URL:https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path} - Use WebFetch to fetch the file content
- Proceed directly to Step 5 for analysis
For Directory Scan:
- Fetch directory:
https://api.github.com/repos/{owner}/{repo}/contents/{path}?ref={branch}- If branch is empty, omit
?ref=parameter (uses default branch) - Use WebFetch with prompt: "Extract the JSON array of files. For each item, return: name, type (file/dir), download_url"
- If branch is empty, omit
- Determine content type and fetch accordingly:
- If
plugin.jsonexists: Full plugin scan (fetch all plugin files) - If
skills/exists: Skill scan (fetch skill directories) - If
SKILL.mdexists: Single skill directory scan (fetch all files in directory) - If none of the above: Error "No scannable content found. Expected plugin.json, skills/ directory, or SKILL.md."
- If
- Recursively fetch required directories:
skills/→ fetch subdirectories → fetchSKILL.mdfilesagents/→ fetch all*.mdfiles (if exists)hooks/→ fetch all*.mdfiles (if exists)commands/→ fetch all*.mdfiles (if exists)
Step 2-URL-3: Fetch File Contents
For plugin scan, fetch:
plugin.json,README.md,.mcp.jsonskills/*/SKILL.md,agents/*.md,hooks/*.md,commands/*.md
For skill directory scan (skills/ or single skill), fetch:
- All files in the skill directory
Use WebFetch with prompt: "Return the raw file content exactly as-is"
Step 2-URL-4: Error Handling
- 404: Repository or path not found
- 403/401: Private repo (not supported) or rate limit exceeded
- Other errors: Report the error message
After fetching all files, proceed to Step 5 for analysis.
Step 3: Get Scan Targets
Based on scope determined in Step 2 and target_agents from Step 1, collect targets:
For plugins (Claude Code only):
User-level:
- Read
~/.claude/plugins/installed_plugins.json .pluginsmaps each plugin ID (<plugin>@<marketplace>) to an array of install records. For each ID, keep the ID and every record'sinstallPath, regardless of the record'sscope, then deduplicate the scan targets byinstallPath- If file doesn't exist, report "No user-level plugins installed"
Project-level:
- Use Glob to find plugins in
.claude/plugins/*/ - If no plugins found, report "No project-level plugins found"
For skills (based on target_agents):
For each agent in target_agents list, collect skills from the corresponding directories:
Agent path mapping:
| Agent | Project Level | User Level |
|-------|---------------|------------|
| claude | .claude/skills/*/ | ~/.claude/skills/*/ |
| codex | .codex/skills/*/ | ~/.codex/skills/*/ |
| gemini | .gemini/skills/*/ | ~/.gemini/skills/*/ |
| agents | .agents/skills/*/ | ~/.config/agents/skills/*/ AND ~/.agents/skills/*/ |
For each agent in target_agents:
User-level:
- Determine user-level path(s) based on agent ID (see table above)
- Find skill directories in the path
- For each skill directory found, note the path and agent ID for scanning
- If no skills found for this agent, report "No user-level skills found for {agent}"
Project-level:
- Determine project-level path based on agent ID (see table above)
- Find skill directories in the path
- For each skill directory found, note the path and agent ID for scanning
- If no skills found for this agent, report "No project-level skills found for {agent}"
Step 4: Filter Targets
If --all flag is set: Skip this step entirely and scan all targets.
Self-exclusion (automatic, every target type)
<skill base directory> is the directory the harness reports as "Base directory for this skill" at this skill's invocation. Do not hardcode an absolute path.
- Normalize before comparing: on both sides, drop a trailing
/and expand a leading~ - Resolving a path: run
ls -ld <dir>. When its output shows-> <target>, the resolved path is<target>when absolute, else<target>joined against the directory containing<dir>and normalized. Ancestor components are not resolved. When the command fails, that path has no resolved form - Self target: a collected target — a plugin install record's
installPath, a project-level plugin directory, or a skill directory — is this scanner itself when its path equals<skill base directory>, or one of the two is a/-boundary prefix of the other. Compare the pair twice: as collected against as reported, then with both sides resolved - Skip every self target. When any of them is a plugin install record, also skip every other record sharing that record's plugin ID. Report each skip as a path match, counted in the Trusted column
- When
<skill base directory>is unavailable: skip plugins and skills namedsecurity-scanner, and report the skip as a name match - A plugin or skill named
security-scannerthat is not this scanner is scanned normally whenever<skill base directory>is available; a second copy of this scanner installed elsewhere on the host is one such target
For plugins:
Trusted sources:
- If the marketplace is in
trusted_marketplaces→ Skip - If the plugin ID (
<plugin>@<marketplace>) is intrusted_plugins→ Skip - Report skipped plugins as "Trusted (skipped)"
For skills:
Trusted sources:
- If the skill name is in
trusted_skills→ Skip (any agent) - Report skipped skills as "Trusted (skipped)"
Step 5: Analyze Each Plugin
For each non-trusted plugin:
-
Read plugin metadata (
plugin.json,README.md) to understand its stated purpose -
Read all executable content:
skills/*/SKILL.md- Skill definitions and instructionsagents/*.md- Agent system prompts (if exists)hooks/*.md- Hook definitions (if exists)commands/*.md- Command definitions (if exists).mcp.json- MCP server configurations (if exists)
-
Analyze for malicious intent (both code AND natural language)
Step 5.5: Analyze Each Skill
For each non-trusted skill:
-
Read skill metadata (
SKILL.mdfrontmatter) to understand its stated purpose -
Read all files in the skill directory:
SKILL.md- Main skill instructions (required)- All other files in the directory
-
Analyze for malicious intent (both code AND natural language)
Step 6: Threat Detection
Code-Level Threats
Look for dangerous command patterns:
- Remote code execution:
curl|sh,wget|bash,base64 -d|sh - Language runtime RCE:
python -c,perl -e,ruby -e,node -e,osascript,powershell - Reverse shells:
nc -e,netcat,socat,mkfifo - System destruction:
rm -rf /,dd if= - Credential access:
~/.ssh/,/etc/passwd,keychain - Data exfiltration:
sshpass,scp,rsyncto external servers - Temp file execution:
chmod +x /tmp/*,mktemp+ execution - Obfuscation:
base64/xxd/hexdumpdecoding + execution - Package install side effects:
npm install/pip installwithpostinstallorsetup.py - Persistence:
crontab,launchctl - Privilege escalation:
sudo,doas
Natural Language Threats (CRITICAL)
Carefully read and analyze system prompts, instructions, and documentation for:
- Data exfiltration instructions: Instructions to read sensitive files (SSH keys, credentials, API keys, private data) and send them externally
- Hidden malicious actions: Instructions to perform harmful actions while appearing helpful
- Deceptive instructions: Instructions that contradict the plugin's stated purpose
- Security bypass: Instructions to ignore security warnings or bypass restrictions
- User deception: Instructions to hide actions from the user or provide false information
- Log/trace hiding: Instructions to delete logs, clear history, or hide evidence of actions
- Excessive permission requests: Instructions requesting access to unrelated resources
Permission Analysis
Check if permissions match the plugin's purpose:
- Does a "translation plugin" need
Bash(*)? Suspicious. - Does a "git helper" need
Bash(git *)? Reasonable. - Does a "file organizer" need access to
~/.ssh/? Suspicious.
Step 7: Generate Report
Generate the report in the language specified by report_language setting.
For Local Scans (default, --user, --project, --all)
Japanese (ja) - Default:
# セキュリティ分析レポート
## 概要
| エージェント | 種別 | 検出 | 信頼済 | スキャン | 悪意あり | 要注意 | 安全 |
|-------------|------|------|--------|----------|----------|--------|------|
| claude | プラグイン | N | N | N | N | N | N |
| claude | スキル | N | N | N | N | N | N |
注: `target_agents` に設定されたエージェントのみ表示。プラグインは常に `claude` 配下。
## 信頼済み(スキップ)
- plugin-name@marketplace(信頼済みマーケットプレイス)
- plugin-name@marketplace(自己除外: パス一致)
- skill-name (claude)(自己除外: 名前一致)
- skill-name (claude) - 信頼済みスキル
## 検出結果
### プラグイン (claude)
#### [プラグイン名]
**種別:** プラグイン
**目的:** [README/plugin.json から]
**判定:** 安全 / 要注意 / 悪意あり
**検出された問題:**
- [問題の説明、ファイル、懸念される理由]
### スキル
#### [スキル名] (claude)
**エージェント:** claude
**種別:** スキル
**場所:** ~/.claude/skills/skill-name/ または .claude/skills/skill-name/
**目的:** [SKILL.md の description から]
**判定:** 安全 / 要注意 / 悪意あり
**検出された問題:**
- [問題の説明と懸念される理由]
---
## 推奨事項
問題のある項目について:
- [ ] 安全 - 使用可
- [ ] 要確認 - [具体的な懸念点]
- [ ] 使用禁止 - [悪意のあるコンテンツを検出]
English (en):
# Security Analysis Report
## Summary
| Agent | Type | Found | Trusted | Scanned | Malicious | Suspicious | Safe |
|-------|------|-------|---------|---------|-----------|------------|------|
| claude | Plugins | N | N | N | N | N | N |
| claude | Skills | N | N | N | N | N | N |
Note: Only rows for configured `target_agents` are shown. Plugins are always under `claude`.
## Trusted (Skipped)
- plugin-name@marketplace (trusted marketplace)
- plugin-name@marketplace (self-exclusion: path match)
- skill-name (claude) (self-exclusion: name match)
- skill-name (claude) - trusted skill
## Findings
### Plugins (claude)
#### [Plugin Name]
**Type:** Plugin
**Purpose:** [from README/plugin.json]
**Verdict:** Safe / Suspicious / Malicious
**Issues found:**
- [Description of issue, file, and why it's concerning]
### Skills
#### [Skill Name] (claude)
**Agent:** claude
**Type:** Skill
**Location:** ~/.claude/skills/skill-name/ or .claude/skills/skill-name/
**Purpose:** [from SKILL.md description]
**Verdict:** Safe / Suspicious / Malicious
**Issues found:**
- [Description of issue and why it's concerning]
For GitHub URL Scans (--url)
Use the same report format as local scans, with this header added:
Japanese (ja):
- URL: {元のURL}
- 種別: プラグイン / スキル / 単一ファイル
English (en):
- URL: {original URL}
- Type: Plugin / Skill / Single file
Analysis Guidelines
- Consider context: A security plugin checking for
rm -rfpatterns is different from a plugin containingrm -rfcommands - Check purpose alignment: Does the code/instruction match what the plugin claims to do?
- Trust but verify: Read the actual content, don't just pattern match
- When uncertain, flag as suspicious
- Explain findings: Always explain WHY something is flagged