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:
mohitagw15856
2026-06-18 08:09:14 +01:00
committed by GitHub
parent 32ff3a96ee
commit e9bc1d0626
33 changed files with 1050 additions and 32 deletions
+120
View File
@@ -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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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 &amp; Hermes. Install all with <code>npx pm-claude-skills add --agent &lt;tool&gt;</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
View File
@@ -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"
+130
View File
@@ -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. ✓');
}