Security auditor, personas, orchestration, docs catalog & roadmap (#35)
Closes the remaining gaps vs alirezarezvani/claude-skills across trust, content types, discoverability, and community. Security (trust signal + useful): - scripts/skill-audit.mjs scans skills/*/SKILL.md + each skill's scripts/ for prompt injection, exfiltration, dynamic code exec, destructive shell, secrets, and hidden text. HIGH fails CI (.github/workflows/skill-audit.yml) + a badge. - New skill-security-auditor skill teaches the same review (production tier). Content types: - output-styles/ — 4 personas (Startup CTO, Growth Marketer, Solo Founder, Product Leader) as Claude Code output styles; --agent claude installs them too. - ORCHESTRATION.md — Skill Chain / Multi-Agent Handoff / Domain Deep-Dive / Solo Sprint patterns. Discoverability: - scripts/build-docs.mjs generates a server-rendered, SEO-indexable web/catalog.html of all skills (built in the Pages deploy; gitignored). Linked from README + playground. Community: - ROADMAP.md (now/next/later + good-first-issues). README badges/sections, TIERS (47 production), CHANGELOG, package.json files, and exports/web index all updated. SkillCheck + security audit + exports verified. Claude-Session: https://claude.ai/code/session_016JWn5jRD5tcEFKrubjQ6Px Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates web/catalog.html — a static, SEO-indexable catalog of every skill,
|
||||
// grouped by bundle, from web/skills.json. Server-rendered HTML so search engines
|
||||
// index each skill's name + description (the playground is client-rendered and
|
||||
// isn't crawlable). Run after web/build-skills.mjs. No dependencies.
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(__dirname, '..');
|
||||
const skillsJson = join(root, 'web', 'skills.json');
|
||||
const REPO = 'https://github.com/mohitagw15856/pm-claude-skills';
|
||||
|
||||
if (!existsSync(skillsJson)) {
|
||||
console.error('web/skills.json not found — run: node web/build-skills.mjs');
|
||||
process.exit(1);
|
||||
}
|
||||
const { skills } = JSON.parse(readFileSync(skillsJson, 'utf8'));
|
||||
|
||||
const esc = (s) => String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
const TIER = {
|
||||
production: ['🟢', 'Production-Ready'],
|
||||
stable: ['🔵', 'Stable'],
|
||||
experimental: ['🟡', 'Experimental'],
|
||||
};
|
||||
|
||||
// Group by bundle, sorted; skills sorted by title within.
|
||||
const byBundle = {};
|
||||
for (const s of skills) (byBundle[s.plugin] ||= []).push(s);
|
||||
const bundles = Object.keys(byBundle).sort();
|
||||
for (const b of bundles) byBundle[b].sort((a, b2) => a.title.localeCompare(b2.title));
|
||||
|
||||
const cards = (list) => list.map((s) => {
|
||||
const [dot, label] = TIER[s.tier] || TIER.stable;
|
||||
return ` <article class="card" id="${esc(s.name)}">
|
||||
<div class="row"><span class="tier tier-${s.tier}">${dot} ${label}</span><span class="bundle">${esc(s.plugin)}</span></div>
|
||||
<h3>${esc(s.title)}</h3>
|
||||
<p>${esc(s.description)}</p>
|
||||
<div class="links">
|
||||
<a href="${REPO}/blob/main/skills/${esc(s.name)}/SKILL.md">SKILL.md ↗</a>
|
||||
<a href="https://mohitagw15856.github.io/pm-claude-skills/#${esc(s.name)}">Run in Playground →</a>
|
||||
</div>
|
||||
</article>`;
|
||||
}).join('\n');
|
||||
|
||||
const sections = bundles.map((b) =>
|
||||
` <section class="bundle-section">\n <h2 id="bundle-${esc(b)}">${esc(b)} <span class="count">${byBundle[b].length}</span></h2>\n${cards(byBundle[b])}\n </section>`
|
||||
).join('\n');
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Skill Catalog — ${skills.length} Agent Skills for Claude, ChatGPT, Gemini, Cursor & more</title>
|
||||
<meta name="description" content="Browse all ${skills.length} professional Agent Skills (SKILL.md) — product, engineering, customer success, marketing, design, finance, HR, sales and more. Works with Claude, ChatGPT, Gemini, Cursor, Codex, Hermes." />
|
||||
<link rel="canonical" href="https://mohitagw15856.github.io/pm-claude-skills/catalog.html" />
|
||||
<style>
|
||||
:root{--bg:#0f1115;--panel:#161a21;--panel2:#1d222b;--border:#2a313c;--text:#e7ebf0;--muted:#95a0b0;--accent:#d97757;--accent2:#e89b82}
|
||||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
|
||||
a{color:var(--accent2);text-decoration:none}a:hover{text-decoration:underline}
|
||||
header{padding:28px 22px;border-bottom:1px solid var(--border);background:var(--panel)}
|
||||
header h1{margin:0 0 6px;font-size:24px}header p{margin:0;color:var(--muted);font-size:14px}
|
||||
.nav{margin-top:12px;display:flex;gap:14px;flex-wrap:wrap;font-size:13px}
|
||||
.controls{position:sticky;top:0;z-index:5;background:var(--bg);padding:14px 22px;border-bottom:1px solid var(--border)}
|
||||
.controls input{width:100%;max-width:520px;padding:10px 12px;background:var(--panel2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px}
|
||||
main{max-width:1100px;margin:0 auto;padding:8px 22px 60px}
|
||||
.bundle-section{margin-top:30px}
|
||||
.bundle-section h2{font-size:16px;border-bottom:1px solid var(--border);padding-bottom:8px;text-transform:uppercase;letter-spacing:.04em;color:var(--accent2)}
|
||||
.count{color:var(--muted);font-size:12px;font-weight:400}
|
||||
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:14px 16px;margin:12px 0}
|
||||
.card h3{margin:6px 0 6px;font-size:16px}.card p{margin:0 0 10px;color:var(--muted);font-size:13.5px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.tier{font-size:10px;font-weight:600;padding:2px 7px;border-radius:99px;border:1px solid transparent}
|
||||
.tier-production{color:#6ee7b7;background:rgba(16,185,129,.12);border-color:rgba(16,185,129,.35)}
|
||||
.tier-stable{color:#93c5fd;background:rgba(59,130,246,.12);border-color:rgba(59,130,246,.35)}
|
||||
.tier-experimental{color:#fcd34d;background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.35)}
|
||||
.bundle{font-size:10.5px;letter-spacing:.03em;text-transform:uppercase;color:var(--accent2);font-weight:600;margin-left:auto}
|
||||
.links{display:flex;gap:14px;font-size:12.5px}
|
||||
.empty{color:var(--muted);padding:40px;text-align:center}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🧠 Skill Catalog — ${skills.length} professional Agent Skills</h1>
|
||||
<p>Structured <code>SKILL.md</code> skills for Claude, ChatGPT, Gemini, Cursor, Codex & Hermes. Install all with <code>npx pm-claude-skills add --agent <tool></code>.</p>
|
||||
<div class="nav">
|
||||
<a href="https://mohitagw15856.github.io/pm-claude-skills/">▶ Live Playground</a>
|
||||
<a href="${REPO}">GitHub</a>
|
||||
<a href="${REPO}#-quick-install-2-minutes">Install</a>
|
||||
<a href="${REPO}/blob/main/TIERS.md">Tiers</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="controls"><input id="q" type="search" placeholder="Filter ${skills.length} skills…" oninput="filter()" /></div>
|
||||
<main id="main">
|
||||
${sections}
|
||||
<p class="empty" id="empty" hidden>No skills match.</p>
|
||||
</main>
|
||||
<script>
|
||||
function filter(){
|
||||
var q=document.getElementById('q').value.toLowerCase().trim();
|
||||
var any=false;
|
||||
document.querySelectorAll('.bundle-section').forEach(function(sec){
|
||||
var shown=0;
|
||||
sec.querySelectorAll('.card').forEach(function(c){
|
||||
var hit=!q||c.textContent.toLowerCase().includes(q);
|
||||
c.hidden=!hit; if(hit){shown++;any=true;}
|
||||
});
|
||||
sec.hidden=shown===0;
|
||||
});
|
||||
document.getElementById('empty').hidden=any;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
writeFileSync(join(root, 'web', 'catalog.html'), html);
|
||||
console.log(`Wrote web/catalog.html — ${skills.length} skills across ${bundles.length} bundles.`);
|
||||
+2
-2
@@ -106,10 +106,10 @@ else
|
||||
count=$((count + 1))
|
||||
done
|
||||
|
||||
# Claude Code also gets subagents and slash commands (siblings of skills/).
|
||||
# Claude Code also gets subagents, slash commands, and output-styles (siblings of skills/).
|
||||
if [ "$AGENT" = "claude" ]; then
|
||||
claude_root="$(dirname "$TARGET")" # ~/.claude
|
||||
for kind in agents commands; do
|
||||
for kind in agents commands output-styles; do
|
||||
src="$REPO_DIR/$kind"
|
||||
[ -d "$src" ] || continue
|
||||
dest="$claude_root/$kind"
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env node
|
||||
// Skill Security Auditor — scans installable skill content (skills/*/SKILL.md and
|
||||
// each skill's scripts/) for patterns that could harm someone who installs them:
|
||||
// prompt injection, data exfiltration, dynamic code execution, destructive shell,
|
||||
// hardcoded secrets, and hidden/obfuscated text.
|
||||
//
|
||||
// Only HIGH-severity findings fail the build; medium/low are advisory. This keeps
|
||||
// it useful without drowning legitimate skills in false positives.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/skill-audit.mjs # audit all skills
|
||||
// node scripts/skill-audit.mjs --json # machine-readable
|
||||
// node scripts/skill-audit.mjs --all # also fail on medium findings
|
||||
//
|
||||
// No dependencies.
|
||||
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
||||
import { join, dirname, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(__dirname, '..');
|
||||
const skillsDir = join(root, 'skills');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const asJson = args.includes('--json');
|
||||
const failOnMedium = args.includes('--all');
|
||||
|
||||
// severity: high (fail), medium, low. Each rule: {id, severity, re, why}
|
||||
const RULES = [
|
||||
// ── Prompt injection aimed at the model ──────────────────────────────────
|
||||
{ id: 'inject.ignore', severity: 'high', why: 'Tries to override the model\'s prior/system instructions.',
|
||||
re: /\b(ignore|disregard|forget)\b[^.\n]{0,40}\b(previous|prior|above|all|earlier|system)\b[^.\n]{0,20}\b(instructions?|prompts?|rules?|guidelines?)/i },
|
||||
{ id: 'inject.devmode', severity: 'high', why: 'Jailbreak framing (developer mode / DAN / no restrictions).',
|
||||
re: /\b(developer mode|do anything now|\bDAN\b|jailbreak|no (restrictions|guardrails|filters)|without (any )?(restrictions|limitations))\b/i },
|
||||
{ id: 'inject.reveal', severity: 'high', why: 'Tries to extract the system prompt / hidden instructions.',
|
||||
re: /\b(reveal|print|show|repeat|output)\b[^.\n]{0,30}\b(system prompt|your (instructions|system message|initial prompt)|hidden (instructions|prompt))/i },
|
||||
{ id: 'inject.persona', severity: 'medium', why: 'Forces an unconstrained persona override.',
|
||||
re: /\byou are now\b[^.\n]{0,40}\b(unrestricted|unfiltered|amoral|evil|no rules)\b/i },
|
||||
|
||||
// ── Data exfiltration ────────────────────────────────────────────────────
|
||||
{ id: 'exfil.send', severity: 'high', why: 'Instructs sending user/conversation data to an external endpoint.',
|
||||
re: /\b(send|post|upload|transmit|exfiltrate|forward)\b[^.\n]{0,40}\b(to )?(https?:\/\/|webhook|api\.|endpoint|server)\b[^.\n]{0,40}\b(conversation|messages?|data|credentials?|keys?|tokens?|history)/i },
|
||||
{ id: 'exfil.beacon', severity: 'medium', why: 'Network call to a hardcoded external URL inside content.',
|
||||
re: /\b(curl|wget|fetch\(|requests\.(get|post)|urllib|http\.client)\b[^.\n]{0,60}https?:\/\/(?!localhost|127\.0\.0\.1|\[|[a-z0-9.-]*example\.(com|org))/i },
|
||||
|
||||
// ── Code / command execution ─────────────────────────────────────────────
|
||||
{ id: 'exec.dynamic', severity: 'medium', why: 'Executes dynamically-built code/commands.',
|
||||
re: /\b(eval|exec)\s*\(|\bos\.system\s*\(|subprocess\.(run|call|Popen)\s*\(|child_process|\bFunction\s*\(\s*['"`]/ },
|
||||
{ id: 'exec.destructive', severity: 'high', why: 'Destructive shell command.',
|
||||
re: /\brm\s+-rf\s+(\/|~|\$HOME|\*)|\b(mkfs|dd\s+if=)|\b:\(\)\s*\{\s*:\|:&\s*\}|\bchmod\s+-R?\s*777\s+\// },
|
||||
|
||||
// ── Credentials / secrets ────────────────────────────────────────────────
|
||||
{ id: 'secret.aws', severity: 'high', why: 'Looks like a hardcoded AWS access key.', re: /\bAKIA[0-9A-Z]{16}\b/ },
|
||||
{ id: 'secret.private-key', severity: 'high', why: 'Embedded private key.', re: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
|
||||
{ id: 'secret.harvest', severity: 'medium', why: 'Asks the user/model to hand over secrets.',
|
||||
re: /\b(send|share|paste|provide|enter)\b[^.\n]{0,30}\b(your )?(api[_ ]?key|password|secret|access token|ssh key|private key|seed phrase)\b/i },
|
||||
|
||||
// ── Obfuscation / hidden text ────────────────────────────────────────────
|
||||
{ id: 'hidden.zerowidth', severity: 'high', why: 'Contains zero-width / invisible Unicode (can hide instructions).',
|
||||
re: /[---]/ },
|
||||
{ id: 'hidden.base64blob', severity: 'medium', why: 'Long base64 blob (possible hidden payload).',
|
||||
re: /\b[A-Za-z0-9+/]{220,}={0,2}\b/ },
|
||||
];
|
||||
|
||||
function auditText(rel, text, findings) {
|
||||
const lines = text.split('\n');
|
||||
for (const rule of RULES) {
|
||||
// search line-by-line so we can report a location and a snippet
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const m = lines[i].match(rule.re);
|
||||
if (m) {
|
||||
findings.push({ file: rel, line: i + 1, id: rule.id, severity: rule.severity, why: rule.why, snippet: lines[i].trim().slice(0, 120) });
|
||||
break; // one hit per rule per file is enough
|
||||
}
|
||||
}
|
||||
// zero-width can sit anywhere incl. between lines — also test whole text
|
||||
if (rule.id === 'hidden.zerowidth' && !findings.some((f) => f.file === rel && f.id === rule.id) && rule.re.test(text)) {
|
||||
findings.push({ file: rel, line: 0, id: rule.id, severity: rule.severity, why: rule.why, snippet: '(invisible characters)' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function walk(dir, exts, out) {
|
||||
for (const e of readdirSync(dir)) {
|
||||
const p = join(dir, e);
|
||||
if (statSync(p).isDirectory()) walk(p, exts, out);
|
||||
else if (exts.some((x) => p.endsWith(x))) out.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Skills whose job is to *document* attack patterns (so they legitimately contain
|
||||
// the phrases the rules look for). Audited by humans, skipped by the scanner.
|
||||
const ALLOWLIST = new Set(['skill-security-auditor']);
|
||||
|
||||
const findings = [];
|
||||
if (existsSync(skillsDir)) {
|
||||
for (const name of readdirSync(skillsDir)) {
|
||||
if (ALLOWLIST.has(name)) continue;
|
||||
const sdir = join(skillsDir, name);
|
||||
if (!statSync(sdir).isDirectory()) continue;
|
||||
const files = [];
|
||||
const skillMd = join(sdir, 'SKILL.md');
|
||||
if (existsSync(skillMd)) files.push(skillMd);
|
||||
const scripts = join(sdir, 'scripts');
|
||||
if (existsSync(scripts)) walk(scripts, ['.py', '.mjs', '.js', '.sh'], files);
|
||||
for (const f of files) auditText(relative(root, f), readFileSync(f, 'utf8'), findings);
|
||||
}
|
||||
}
|
||||
|
||||
const counts = findings.reduce((a, f) => ((a[f.severity] = (a[f.severity] || 0) + 1), a), {});
|
||||
const high = counts.high || 0, medium = counts.medium || 0, low = counts.low || 0;
|
||||
|
||||
if (asJson) {
|
||||
console.log(JSON.stringify({ scanned: 'skills/**', high, medium, low, findings }, null, 2));
|
||||
} else {
|
||||
const icon = { high: '🔴', medium: '🟠', low: '🟡' };
|
||||
for (const f of findings.sort((a, b) => (a.severity < b.severity ? -1 : 1))) {
|
||||
console.log(` ${icon[f.severity]} [${f.severity}] ${f.file}:${f.line} (${f.id}) — ${f.why}`);
|
||||
if (f.snippet) console.log(` ↳ ${f.snippet}`);
|
||||
}
|
||||
console.log(`\nSkill Security Audit — ${high} high · ${medium} medium · ${low} low across skills/**`);
|
||||
}
|
||||
|
||||
const failed = high > 0 || (failOnMedium && medium > 0);
|
||||
if (failed) {
|
||||
if (!asJson) console.log('FAILED — review the findings above. (False positive? Tune scripts/skill-audit.mjs.)');
|
||||
process.exit(1);
|
||||
} else if (!asJson) {
|
||||
console.log('No high-severity issues found. ✓');
|
||||
}
|
||||
Reference in New Issue
Block a user