mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-10-06 02:50:08 +00:00
The user asked us to move everything from alirezarezvani/aeo-box into this
repo, audit every part, rebuild improved, polish for human users.
Audit identified 4 distinct components in aeo-box:
1. **AEO Skill** (`answer-engine-optimization/`) — 9 Python modules, 2,464
LOC, comprehensive SKILL.md. Real value: Answer Engine Optimization is
its own discipline distinct from SEO.
2. **Security-guidance hook** — David Dworken's MIT-licensed PreToolUse
hook catching 9 security anti-patterns in Edit/Write/MultiEdit. Hook-
based plugin pattern we don't have in our repo yet.
3. **Agentic AEO master prompt** — 1,579-line spec for a multi-agent
AEO application using Claude Agent SDK. Future-work spec.
4. **Generic dev infra** — 11 generic agents + 9 generic commands +
GH workflows + TS scripts. We already have equivalents; not worth
porting.
**This PR delivers 1, 2, and 3** (skipping 4 as planned).
## 1. AEO Skill — `marketing-skill/skills/aeo/`
Distilled 9-module Python toolkit into 3 stdlib CLI tools per
claude-skills convention:
- `aeo_audit.py` (445 LOC) — E-E-A-T + structure scoring across 4
dimensions + structure. Composite 0-100 with letter grade. 8 industries
with calibrated thresholds (healthcare/finance/legal 85+ for YMYL;
saas/b2b/media 70; ecommerce 65). Industry-aware top-fix
recommendations.
- `aeo_optimizer.py` (252 LOC) — Generates AEO-improved variants in 3
modes: conservative (schema + footer only), balanced (citation markers
+ heading restructure + schema), aggressive (fact-first lede + full
restructure). Schema.org Article + FAQPage JSON-LD auto-generated.
- `citation_tracker.py` (310 LOC) — Local-first citation ledger at
~/.aeo-data/citations.json. add/list/report/export actions. Computes
per-URL stats: citation count, LLM coverage, velocity, top queries,
verdict (EARLY/EMERGING/STRONG).
Plus:
- `SKILL.md` — Path-B-style spec with workflow, industry table,
anti-patterns, dependencies
- 3 references citing 8 sources each:
- `aeo_eeat_canon.md` — E-E-A-T methodology for AI citation
- `llm_citation_patterns.md` — per-LLM citation behavior
(Perplexity, ChatGPT, Claude, Gemini, Mistral)
- `aeo_vs_seo.md` — strategic choice between disciplines
- `cs-aeo` agent persona — pragmatic content strategist; refuses fake
authority signals; insists on real first-person evidence
- `/cs:aeo` command with audit/optimize/track/report/export actions
All 3 scripts smoke-tested:
- aeo_audit --sample → 43/100 (F) on intentionally-weak sample content
- aeo_optimizer --sample → schema injected + corrections footer added
+ bold-paragraphs promoted to H3 + 1 citation marker
- citation_tracker --sample → 4-event sequence (add → list → report
→ export), verdict EMERGING with 4 citations across 3 LLMs
## 2. Security-guidance hook — `engineering/security-guidance/`
PreToolUse hook ported from David Dworken's MIT implementation. Preserves
the 9 upstream patterns verbatim + adds 3 new patterns:
| Pattern | Upstream | Added |
|---|:-:|:-:|
| GitHub Actions workflow injection | ✓ | |
| child_process.exec / execSync | ✓ | |
| new Function | ✓ | |
| eval() | ✓ | |
| dangerouslySetInnerHTML | ✓ | |
| document.write | ✓ | |
| .innerHTML = | ✓ | |
| pickle | ✓ | |
| os.system | ✓ | |
| subprocess shell=True | | ✓ |
| SQL via f-string or .format | | ✓ |
| yaml.unsafe_load | | ✓ |
Modifications from upstream:
- Debug log moved from /tmp to ~/.claude/security-warnings-log.txt
(persists across reboots)
- Restructured as claude-skills plugin with `attribution` block in
plugin.json (matches caveman/grill-me/grill-with-docs pattern)
- Added comprehensive reference doc: pretooluse_hook_canon.md
(8 sources on hook design discipline)
Hook smoke-tested:
- eval(input()) in Write → exits 2 (BLOCK) with stderr warning ✓
- json.loads(input()) in Write → exits 0 (clean) ✓
- subprocess.run(cmd, shell=True) fresh session → exits 2 ✓
- subprocess.run(cmd, shell=True) cached session → exits 0 ✓
(correct UX: warned once, don't nag)
## 3. Master prompt preserved — `megaprompts/14-aeo-agentic-megaprompt.md`
The 1,579-line multi-agent AEO application spec preserved verbatim as
megaprompts/14 — the next slot after 13-research-megaprompt.md. Path-B
option open for future "build the full agentic AEO app" work.
## Cross-platform sync
- marketplace.json: 55 → 57 plugins (`aeo` + `security-guidance`)
- .codex/skills-index.json: 303 → 305 entries (both new skills indexed)
- .codex/skills/: aeo + security-guidance symlinks created
- .gemini/skills-index.json: 353 → 355 entries
- .gemini/skills/aeo, .gemini/skills/security-guidance: directory mirrors
- .hermes/skills/claude-skills/: re-synced (now includes both new skills
with relative symlinks)
## What's NOT ported (intentional)
- 11 generic agents from aeo-box .claude/agents/ — we have equivalents
(cs-code-reviewer, cs-senior-engineer, cs-skill-author)
- 9 generic commands from .claude/commands/ — we have /git:cm /git:cp
/cs:write-a-skill, etc.
- GitHub workflow YAMLs — repo-specific
- TS scripts (auto-close-duplicates, backfill-duplicate-comments) — GH
issue management, not a skill
Documented this skip-list in the AEO SKILL.md `Source` block + the
security-guidance plugin.json `attribution` block.
## Honest BYO-sync clarifier for Hermes (folded in from prior work)
Earlier merged PR #678 upgraded Hermes Agent integration to first-class
technical support (committed .hermes/ tree, fixed sync script, relative
symlinks). The earlier docs sweep added an install/configure walkthrough
that wasn't in scope for that PR but caught a user-flagged gap. That doc
section is also in this commit (137 lines added to docs/integrations.md
covering: Hermes-itself install steps, first-run walkthrough,
configuration tips, 6 troubleshooting Q&A).
Verification:
- All 3 AEO scripts pass --help and --sample
- Security hook correctly exits 2 on detection, 0 on cached/clean
- All 3 cross-platform syncs ran clean
- marketplace.json: 57 plugins, all required fields, no duplicates
- 13 v2.7.0 + 13 new files for AEO + 6 new files for security-guidance
+ 1 megaprompt + 1 docs update
https://claude.ai/code/session_01FEUmeuYhmnxVFq7EZM8ZSw
291 lines
12 KiB
Python
291 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Security Reminder Hook for Claude Code
|
|
|
|
PreToolUse hook that checks Edit/Write/MultiEdit operations for security
|
|
anti-patterns and emits a structured warning before the tool runs.
|
|
|
|
Ported from David Dworken's MIT-licensed plugin at:
|
|
https://github.com/alirezarezvani/aeo-box/blob/main/.claude/plugins/security-guidance/
|
|
|
|
Modifications from upstream:
|
|
- Verbatim pattern table preserved (12 patterns)
|
|
- Session-state caching unchanged (one warning per file+rule per session)
|
|
- 30-day state-file cleanup unchanged
|
|
- Debug log location moved from /tmp to ~/.claude/security-warnings-log.txt
|
|
(more portable, persists across reboots)
|
|
- ENABLE_SECURITY_REMINDER=0 env var disables the hook (unchanged)
|
|
|
|
Wire-up: see hooks.json in the same directory.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import random
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Debug log location (moved to ~/.claude/ for persistence)
|
|
DEBUG_LOG_FILE = str(Path.home() / ".claude" / "security-warnings-log.txt")
|
|
|
|
|
|
def debug_log(message):
|
|
"""Append debug message to log file with timestamp."""
|
|
try:
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
Path(DEBUG_LOG_FILE).parent.mkdir(parents=True, exist_ok=True)
|
|
with open(DEBUG_LOG_FILE, "a") as f:
|
|
f.write(f"[{timestamp}] {message}\n")
|
|
except Exception:
|
|
# Silently ignore logging errors to avoid disrupting the hook
|
|
pass
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# Security patterns (verbatim from upstream — David Dworken @ Anthropic)
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
SECURITY_PATTERNS = [
|
|
{
|
|
"ruleName": "github_actions_workflow",
|
|
"path_check": lambda path: ".github/workflows/" in path
|
|
and (path.endswith(".yml") or path.endswith(".yaml")),
|
|
"reminder": """You are editing a GitHub Actions workflow file. Be aware of these security risks:
|
|
|
|
1. **Command Injection**: Never use untrusted input (like issue titles, PR descriptions, commit messages) directly in run: commands without proper escaping
|
|
2. **Use environment variables**: Instead of ${{ github.event.issue.title }}, use env: with proper quoting
|
|
3. **Review the guide**: https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/
|
|
|
|
Example of UNSAFE pattern to avoid:
|
|
run: echo "${{ github.event.issue.title }}"
|
|
|
|
Example of SAFE pattern:
|
|
env:
|
|
TITLE: ${{ github.event.issue.title }}
|
|
run: echo "$TITLE"
|
|
|
|
Other risky inputs to be careful with:
|
|
- github.event.issue.body
|
|
- github.event.pull_request.title
|
|
- github.event.pull_request.body
|
|
- github.event.comment.body
|
|
- github.event.review.body
|
|
- github.event.review_comment.body
|
|
- github.event.pages.*.page_name
|
|
- github.event.commits.*.message
|
|
- github.event.head_commit.message
|
|
- github.event.head_commit.author.email
|
|
- github.event.head_commit.author.name
|
|
- github.event.commits.*.author.email
|
|
- github.event.commits.*.author.name
|
|
- github.event.pull_request.head.ref
|
|
- github.event.pull_request.head.label
|
|
- github.event.pull_request.head.repo.default_branch
|
|
- github.head_ref""",
|
|
},
|
|
{
|
|
"ruleName": "child_process_exec",
|
|
"substrings": ["child_process.exec", "exec(", "execSync("],
|
|
"reminder": """⚠️ Security Warning: Using child_process.exec() can lead to command injection vulnerabilities.
|
|
|
|
Instead of:
|
|
exec(`command ${userInput}`)
|
|
|
|
Use:
|
|
execFile('command', [userInput]) // Node built-in; no shell
|
|
|
|
execFile (or spawn with shell:false):
|
|
- Prevents shell injection
|
|
- Handles arguments as a list (no interpolation)
|
|
- Recommended whenever you don't actually need shell features
|
|
|
|
Only use exec() if you absolutely need shell features AND the input is guaranteed to be safe (e.g., from a hardcoded allowlist).""",
|
|
},
|
|
{
|
|
"ruleName": "new_function_injection",
|
|
"substrings": ["new Function"],
|
|
"reminder": "⚠️ Security Warning: Using new Function() with dynamic strings can lead to code injection vulnerabilities. Consider alternative approaches that don't evaluate arbitrary code. Only use new Function() if you truly need to evaluate arbitrary dynamic code.",
|
|
},
|
|
{
|
|
"ruleName": "eval_injection",
|
|
"substrings": ["eval("],
|
|
"reminder": "⚠️ Security Warning: eval() executes arbitrary code and is a major security risk. Consider using JSON.parse() for data parsing or alternative design patterns that don't require code evaluation. Only use eval() if you truly need to evaluate arbitrary code.",
|
|
},
|
|
{
|
|
"ruleName": "react_dangerously_set_html",
|
|
"substrings": ["dangerouslySetInnerHTML"],
|
|
"reminder": "⚠️ Security Warning: dangerouslySetInnerHTML can lead to XSS vulnerabilities if used with untrusted content. Ensure all content is properly sanitized using an HTML sanitizer library like DOMPurify, or use safe alternatives.",
|
|
},
|
|
{
|
|
"ruleName": "document_write_xss",
|
|
"substrings": ["document.write"],
|
|
"reminder": "⚠️ Security Warning: document.write() can be exploited for XSS attacks and has performance issues. Use DOM manipulation methods like createElement() and appendChild() instead.",
|
|
},
|
|
{
|
|
"ruleName": "innerHTML_xss",
|
|
"substrings": [".innerHTML =", ".innerHTML="],
|
|
"reminder": "⚠️ Security Warning: Setting innerHTML with untrusted content can lead to XSS vulnerabilities. Use textContent for plain text or safe DOM methods for HTML content. If you need HTML support, consider using an HTML sanitizer library such as DOMPurify.",
|
|
},
|
|
{
|
|
"ruleName": "pickle_deserialization",
|
|
"substrings": ["pickle"],
|
|
"reminder": "⚠️ Security Warning: Using pickle with untrusted content can lead to arbitrary code execution. Consider using JSON or other safe serialization formats instead. Only use pickle if it is explicitly needed or requested by the user.",
|
|
},
|
|
{
|
|
"ruleName": "os_system_injection",
|
|
"substrings": ["os.system", "from os import system"],
|
|
"reminder": "⚠️ Security Warning: This code appears to use os.system. This should only be used with static arguments and never with arguments that could be user-controlled.",
|
|
},
|
|
{
|
|
"ruleName": "subprocess_shell_true",
|
|
"substrings": ["shell=True", "shell = True"],
|
|
"reminder": "⚠️ Security Warning: subprocess with shell=True can lead to command injection. Pass args as a list instead and omit shell=True (the default). Only use shell=True for static commands without user input.",
|
|
},
|
|
{
|
|
"ruleName": "sql_format_string",
|
|
"substrings": [".format(", "f\"SELECT", "f'SELECT", "f\"INSERT", "f'INSERT", "f\"UPDATE", "f'UPDATE", "f\"DELETE", "f'DELETE"],
|
|
"reminder": "⚠️ Security Warning: Building SQL queries with .format() or f-strings can lead to SQL injection. Use parameterized queries (cursor.execute(sql, params)) or an ORM. Only inline values if they come from a trusted, validated source.",
|
|
},
|
|
{
|
|
"ruleName": "yaml_unsafe_load",
|
|
"substrings": ["yaml.load(", "yaml.unsafe_load"],
|
|
"reminder": "⚠️ Security Warning: yaml.load() without Loader= or yaml.unsafe_load() can execute arbitrary code. Use yaml.safe_load() instead, which only parses standard YAML types.",
|
|
},
|
|
]
|
|
|
|
|
|
def get_state_file(session_id):
|
|
"""Get session-specific state file path."""
|
|
return os.path.expanduser(f"~/.claude/security_warnings_state_{session_id}.json")
|
|
|
|
|
|
def cleanup_old_state_files():
|
|
"""Remove state files older than 30 days."""
|
|
try:
|
|
state_dir = os.path.expanduser("~/.claude")
|
|
if not os.path.exists(state_dir):
|
|
return
|
|
|
|
current_time = datetime.now().timestamp()
|
|
thirty_days_ago = current_time - (30 * 24 * 60 * 60)
|
|
|
|
for filename in os.listdir(state_dir):
|
|
if filename.startswith("security_warnings_state_") and filename.endswith(".json"):
|
|
file_path = os.path.join(state_dir, filename)
|
|
try:
|
|
file_mtime = os.path.getmtime(file_path)
|
|
if file_mtime < thirty_days_ago:
|
|
os.remove(file_path)
|
|
except (OSError, IOError):
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def load_state(session_id):
|
|
"""Load the state of shown warnings from file."""
|
|
state_file = get_state_file(session_id)
|
|
if os.path.exists(state_file):
|
|
try:
|
|
with open(state_file, "r") as f:
|
|
return set(json.load(f))
|
|
except (json.JSONDecodeError, IOError):
|
|
return set()
|
|
return set()
|
|
|
|
|
|
def save_state(session_id, shown_warnings):
|
|
"""Save the state of shown warnings to file."""
|
|
state_file = get_state_file(session_id)
|
|
try:
|
|
os.makedirs(os.path.dirname(state_file), exist_ok=True)
|
|
with open(state_file, "w") as f:
|
|
json.dump(list(shown_warnings), f)
|
|
except IOError as e:
|
|
debug_log(f"Failed to save state file: {e}")
|
|
|
|
|
|
def check_patterns(file_path, content):
|
|
"""Check if file path or content matches any security patterns.
|
|
|
|
Returns (ruleName, reminder) on match, or (None, None).
|
|
"""
|
|
normalized_path = file_path.lstrip("/")
|
|
|
|
for pattern in SECURITY_PATTERNS:
|
|
if "path_check" in pattern and pattern["path_check"](normalized_path):
|
|
return pattern["ruleName"], pattern["reminder"]
|
|
|
|
if "substrings" in pattern and content:
|
|
for substring in pattern["substrings"]:
|
|
if substring in content:
|
|
return pattern["ruleName"], pattern["reminder"]
|
|
|
|
return None, None
|
|
|
|
|
|
def extract_content_from_input(tool_name, tool_input):
|
|
"""Extract the content that will be written/edited."""
|
|
if tool_name == "Write":
|
|
return tool_input.get("content", "")
|
|
elif tool_name == "Edit":
|
|
return tool_input.get("new_string", "")
|
|
elif tool_name == "MultiEdit":
|
|
edits = tool_input.get("edits", [])
|
|
if edits:
|
|
return " ".join(edit.get("new_string", "") for edit in edits)
|
|
return ""
|
|
return ""
|
|
|
|
|
|
def main():
|
|
"""Main hook function."""
|
|
# Check if security reminders are enabled
|
|
if os.environ.get("ENABLE_SECURITY_REMINDER", "1") == "0":
|
|
sys.exit(0)
|
|
|
|
# Periodically clean up old state files (10% chance per run)
|
|
if random.random() < 0.1:
|
|
cleanup_old_state_files()
|
|
|
|
# Read hook input from stdin
|
|
try:
|
|
input_data = json.loads(sys.stdin.read())
|
|
except json.JSONDecodeError as e:
|
|
debug_log(f"JSON decode error: {e}")
|
|
sys.exit(0)
|
|
|
|
session_id = input_data.get("session_id", "default")
|
|
tool_name = input_data.get("tool_name", "")
|
|
tool_input = input_data.get("tool_input", {})
|
|
|
|
# Only run on file-edit tools
|
|
if tool_name not in ["Edit", "Write", "MultiEdit"]:
|
|
sys.exit(0)
|
|
|
|
file_path = tool_input.get("file_path", "")
|
|
if not file_path:
|
|
sys.exit(0)
|
|
|
|
content = extract_content_from_input(tool_name, tool_input)
|
|
|
|
rule_name, reminder = check_patterns(file_path, content)
|
|
|
|
if rule_name and reminder:
|
|
# Suppress duplicates within the same session for the same file+rule
|
|
warning_key = f"{file_path}-{rule_name}"
|
|
shown_warnings = load_state(session_id)
|
|
|
|
if warning_key not in shown_warnings:
|
|
shown_warnings.add(warning_key)
|
|
save_state(session_id, shown_warnings)
|
|
# Emit warning + block the tool. Exit code 2 = block in PreToolUse hooks.
|
|
print(reminder, file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|