Security Guidance — Vulnerability Pattern Detection Hook
Vulnerability Pattern Detection Hook
PreToolUse hook monitoring 9 security vulnerability patterns before file edits. Uses session-scoped state to prevent warning fatigue while ensuring dangerous patterns are always flagged.
🛡️ Security🔍 Pattern Detection🐍 Python Hook⚠️ Context-Aware Warnings
📚 What is this & why does it exist?
Security Guidance is a PreToolUse hook that acts as a real-time security linter for Claude Code. Before any file edit is applied, the Python hook scans the target file path and content for 9 known vulnerability patterns — from command injection in GitHub Actions to eval() and pickle deserialization. It uses session-scoped state tracking to avoid showing the same warning repeatedly, and auto-cleans stale state after 30 days. The goal is to catch security anti-patterns at the moment they’re introduced, not days later in a code review.
💡 Real-world analogy
Think of a spell-checker that only checks for dangerous words — not typos, but things like “rm -rf /” or “eval(user_input)”. Just as a spell-checker underlines problematic words as you type them, the Security Guidance hook underlines dangerous patterns the moment Claude tries to write them into a file. And like a smart spell-checker that stops highlighting a word after you’ve explicitly accepted it, the state tracker remembers which warnings you’ve already seen so it doesn’t nag you about the same pattern twice in one session.
Architecture
The hook intercepts file-edit tool calls before they execute. The Python script parses the tool input, matches file paths and content against 9 security pattern rules, checks session state to avoid duplicate warnings, and returns advisory messages when new patterns are detected.
flowchart LR
subgraph Trigger["PreToolUse Event"]
A["Claude requests file edit"] --> B["PreToolUse fires"]
end
subgraph Hook["security_reminder_hook.py"]
B --> C["Parse tool input"]
C --> D["Match against 9 patterns"]
D --> E{"New pattern\ndetected?"}
E -->|"Yes"| F["Load session state"]
F --> G{"Already\nwarned?"}
G -->|"No"| H["Show warning + update state"]
G -->|"Yes"| I["Skip — no warning"]
E -->|"No"| I
end
subgraph Output["Result"]
H --> J[" Security advisory in response"]
I --> K["Silent pass-through"]
end
Security Patterns
The hook monitors for 9 distinct vulnerability patterns, each with specific file path matching and substring detection rules:
| # | Pattern | File Path Match | Substring / Indicator | Risk |
|---|---|---|---|---|
| 1 | Command Injection (GitHub Actions) | .github/workflows/ | ${{ in run: blocks | Arbitrary command execution via PR titles/branch names |
| 2 | child_process.exec | *.js, *.ts | child_process.exec | Shell injection via unsanitized arguments |
| 3 | new Function() | *.js, *.ts | new Function( | Dynamic code execution, equivalent to eval |
| 4 | eval() | *.js, *.ts, *.py | eval( | Arbitrary code execution from string input |
| 5 | dangerouslySetInnerHTML | *.jsx, *.tsx | dangerouslySetInnerHTML | XSS via unsanitized HTML injection in React |
| 6 | document.write() | *.js, *.ts, *.html | document.write( | DOM-based XSS and document clobbering |
| 7 | .innerHTML | *.js, *.ts | .innerHTML | XSS via unsanitized content assignment |
| 8 | pickle | *.py | pickle.load, pickle.loads | Arbitrary code execution during deserialization |
| 9 | os.system | *.py | os.system( | Shell injection via unsanitized command strings |
State Management
Session-Scoped Warning Tracking
To prevent warning fatigue, the hook maintains a state file that tracks which pattern–file combinations have already triggered warnings in the current session. The state file is stored alongside the hook script and is automatically cleaned up after 30 days of inactivity.
📋 State File Format
JSON dictionary mapping pattern keys to timestamps of when the warning was last shown.
- Key format:
{pattern_id}:{file_path} - Value: ISO 8601 timestamp
- Location:
.security-state.jsonnext to hook
🗑️ Auto-Cleanup
Stale entries are removed on each hook invocation to keep the state file compact.
- Entries older than 30 days are pruned
- Cleanup runs before pattern matching
- Zero-entry state file is deleted entirely
{
"eval:src/utils/parser.js": "2026-03-25T14:30:00Z",
"innerHTML:src/components/Render.tsx": "2026-03-25T14:32:00Z",
"pickle:ml/model_loader.py": "2026-03-24T09:15:00Z"
}
Data Flow
sequenceDiagram
participant Claude as Claude LLM
participant CC as Claude Code CLI
participant Hook as security_reminder_hook.py
participant State as .security-state.json
participant Dev as Developer
Claude->>CC: Request: edit file src/api/handler.js
CC->>Hook: PreToolUse(tool="file_edit", input={path, content})
Hook->>Hook: Match path *.js against 9 patterns
Hook->>Hook: Found: child_process.exec in content
Hook->>State: Load existing warnings
State-->>Hook: No prior warning for exec:src/api/handler.js
Hook->>State: Write new entry with timestamp
Hook-->>CC: "child_process.exec detected — consider execFile() or spawn() with array args"
CC-->>Dev: Show security advisory
CC->>Claude: Proceed with file edit + advisory context
Note over Claude,Dev: Later in same session...
Claude->>CC: Request: edit same file again
CC->>Hook: PreToolUse(tool="file_edit", input={path, content})
Hook->>State: Load existing warnings
State-->>Hook: Already warned for exec:src/api/handler.js
Hook-->>CC: Silent pass-through (no duplicate warning)
Key Components
| Component | File | Role |
|---|---|---|
security_reminder_hook.py | hooks-handlers/security_reminder_hook.py | Python PreToolUse hook — pattern matching engine with state tracking |
plugin.json | .claude-plugin/plugin.json | Plugin manifest declaring the PreToolUse hook with file_edit matcher |
README.md | README.md | Plugin documentation, pattern catalog, and configuration guide |
Hook Implementation
PreToolUse Hook Configuration
| Property | Value |
|---|---|
| Event | PreToolUse |
| Type | command |
| Handler | python3 hooks-handlers/security_reminder_hook.py |
| Matcher | file_edit|write_to_file |
| Output | JSON with optional message field for advisory text |
#!/usr/bin/env python3
"""PreToolUse hook: scan file edits for 9 security vulnerability patterns."""
import json, sys, os, datetime
PATTERNS = [
{"id": "gh-actions-injection", "path": ".github/workflows/", "substr": "${{",
"msg": "GitHub Actions command injection — avoid interpolating untrusted input in run: blocks"},
{"id": "child-process-exec", "path_ext": [".js", ".ts"], "substr": "child_process.exec",
"msg": "child_process.exec is vulnerable to shell injection — use execFile() or spawn()"},
{"id": "new-function", "path_ext": [".js", ".ts"], "substr": "new Function(",
"msg": "new Function() enables dynamic code execution — equivalent to eval()"},
{"id": "eval", "path_ext": [".js", ".ts", ".py"], "substr": "eval(",
"msg": "eval() executes arbitrary code — use JSON.parse(), ast.literal_eval(), or a safe parser"},
{"id": "dangerouslySetInnerHTML", "path_ext": [".jsx", ".tsx"], "substr": "dangerouslySetInnerHTML",
"msg": "dangerouslySetInnerHTML enables XSS — sanitize with DOMPurify first"},
{"id": "document-write", "path_ext": [".js", ".ts", ".html"], "substr": "document.write(",
"msg": "document.write() enables DOM-based XSS — use textContent or createElement"},
{"id": "innerHTML", "path_ext": [".js", ".ts"], "substr": ".innerHTML",
"msg": ".innerHTML enables XSS — use textContent for text or sanitize HTML first"},
{"id": "pickle", "path_ext": [".py"], "substr": "pickle.load",
"msg": "pickle deserialization executes arbitrary code — use json or a safe format"},
{"id": "os-system", "path_ext": [".py"], "substr": "os.system(",
"msg": "os.system() is vulnerable to shell injection — use subprocess.run() with a list"},
]
STATE_FILE = os.path.join(os.path.dirname(__file__), ".security-state.json")
Interactions
| Module | Interface | Data Exchanged |
|---|---|---|
| Claude Code CLI | Hook registration | PreToolUse event with file_edit matcher triggers Python script |
| Claude LLM | Advisory injection | Warning messages added to tool response context |
| File system | State persistence | .security-state.json tracks shown warnings with timestamps |
| Developer | Security feedback | ⚠️ inline advisories when dangerous patterns are detected |
Configuration Reference
| Parameter | Default | Description |
|---|---|---|
hooks.PreToolUse[0].type | command | Execution type — shell command invoking Python |
hooks.PreToolUse[0].command | python3 hooks-handlers/security_reminder_hook.py | Path to the security scanner hook |
hooks.PreToolUse[0].matcher | file_edit|write_to_file | Regex matching tool names that trigger the hook |
| State file location | .security-state.json | Relative to hook script directory |
| State TTL | 30 days | Entries older than 30 days are auto-pruned |
| Pattern count | 9 | Number of vulnerability patterns monitored |
Directory Map
Source tree for Security Guidance
security-guidance/
├── .claude-plugin/plugin.json ← Role: Plugin manifest with PreToolUse hook
├── README.md ← Role: Pattern catalog and usage docs
└── hooks-handlers/
├── security_reminder_hook.py ← Role: Pattern detection + state engine
└── .security-state.json ← Role: Session warning state (auto-generated)
Warning Examples
Example Security Advisory
When Claude attempts to edit a file containing a detected pattern:
⚠️ Security Advisory — child_process.exec
The filesrc/api/handler.jsuseschild_process.exec()which passes arguments through a shell, making it vulnerable to command injection.
Recommended: Usechild_process.execFile()orchild_process.spawn()with an argument array to avoid shell interpretation of special characters.
GitHub Actions Pattern
# ⚠️ DANGEROUS — PR title is attacker-controlled
- name: Greet
run: echo "Hello ${{ github.event.pull_request.title }}"
# ✅ SAFE — use an environment variable
- name: Greet
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Hello $PR_TITLE"