Documentation
Hook
Hooks
What stale-docs runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add SectionTN/stale-docs > /plugin install stale-docs@stale-docs
Ships with stale-docs. Installing the plugin gets these hooks.
What fires, and when
PostToolUse
- Matches
Edit|Write|MultiEditnode "${CLAUDE_PLUGIN_ROOT}/hooks/check-stale.js"
Where it lives
- hooks/check-stale.jsRunsGitHub
Read the script
#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); const DEFAULTS = { enabled: true, sourceGlobs: ['**/*.{js,jsx,ts,tsx,mjs,cjs,py,go,rs,java,rb,c,h,cpp,hpp}'], docGlobs: ['README.md', '*.md', 'docs/**/*.md'], ignore: [ '**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/vendor/**', '**/target/**', ], }; const MAX_DOC_FILES = 500; const MAX_SOURCE_FILES = 2000; const MAX_FILE_SIZE = 1024 * 1024; const MAX_SYMBOLS_PER_FILE = 50; const MAX_HOOK_FINDINGS = 20; const AUDIT_LIMIT = 10; const MIN_SYMBOL_LENGTH = 3; // orphans outrank every reference tier: the scanner proved the target is gone const ORPHAN_CONFIDENCE = 5; const SOURCE_EXT = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|go|rs|java|rb|c|h|cpp|hpp)$/; const BACKTICK_TOKEN = /`([^`\n]+)`/g; const IDENT_TOKEN = /[A-Za-z_$][\w$]*/g; // a doc this saturated with references is probably *about* the changed file const REWRITE_MIN_LINES = 3; const REWRITE_DENSITY = 0.3; // identifiers too generic to prove a doc refers to *this* file const STOP_WORDS = new Set([ 'main', 'init', 'new', 'get', 'set', 'run', 'test', 'index', 'data', 'name', 'type', 'value', 'error', 'result', 'args', 'options', 'config', 'default', 'start', 'stop', 'update', 'create', 'delete', 'read', 'write', 'app', 'use', 'add', 'remove', 'list', 'item', 'key', 'state', 'props', ]); const SYMBOL_PATTERNS = [ /\bexport\s+(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/g, /\bexport\s+(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/g, /\bexport\s+(?:const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)/g, /\b(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=/g, /^def\s+([A-Za-z_]\w*)/gm, /^(?:export\s+)?class\s+([A-Za-z_]\w*)/gm, /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm, /\bpub\s+(?:async\s+)?fn\s+([A-Za-z_]\w*)/g, /\bpub\s+(?:struct|enum|trait)\s+([A-Za-z_]\w*)/g, ]; const FLAG_PATTERN = /["'`](--[a-z][a-z0-9-]{2,})["'`]/g; function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function globToRegExp(glob) { let re = ''; let i = 0; while (i < glob.length) { const c = glob[i]; if (c === '*') { if (glob[i + 1] === '*') { if (glob[i + 2] === '/') { re += '(?:[^/]+/)*'; i += 3; } else { re += '.*'; i += 2; } } else { re += '[^/]*'; i += 1; } } else if (c === '{') { const end = glob.indexOf('}', i); if (end === -1) { re += '\\{'; i += 1; } else { const parts = glob.slice(i + 1, end).split(',').map(escapeRegExp); re += '(?:' + parts.join('|') + ')'; i = end + 1; } } else if (c === '?') { re += '[^/]'; i += 1; } else { re += escapeRegExp(c); i += 1; } } return new RegExp('^' + re + '$'); } function compileGlobs(globs) { return globs.map(globToRegExp); } function matchesAny(relPath, regexps) { return regexps.some((re) => re.test(relPath)); } function loadConfig(root) { try { const raw = fs.readFileSync(path.join(root, '.stale-docs.json'), 'utf8'); const parsed = JSON.parse(raw); return { enabled: parsed.enabled !== false, sourceGlobs: Array.isArray(parsed.sourceGlobs) ? parsed.sourceGlobs : DEFAULTS.sourceGlobs, docGlobs: Array.isArray(parsed.docGlobs) ? parsed.docGlobs : DEFAULTS.docGlobs, ignore: Array.isArray(parsed.ignore) ? parsed.ignore : DEFAULTS.ignore, }; } catch { return DEFAULTS; } } function walk(root, ignoreRes, limit) { const out = []; const stack = ['.']; while (stack.length && out.length < limit) { const rel = stack.pop(); let entries; try { entries = fs.readdirSync(path.join(root, rel), { withFileTypes: true }); } catch { continue; } for (const entry of entries) { const childRel = rel === '.' ? entry.name : rel + '/' + entry.name; if (matchesAny(entry.isDirectory() ? childRel + '/' : childRel, ignoreRes)) continue; if (entry.isDirectory()) { stack.push(childRel); } else if (entry.isFile()) { out.push(childRel); if (out.length >= limit) break; } } } return out; } function readSmallFile(absPath) { try { const stat = fs.statSync(absPath); if (!stat.isFile() || stat.size > MAX_FILE_SIZE) return null; return fs.readFileSync(absPath, 'utf8'); } catch { return null; } } function extractSymbols(content) { const symbols = new Set(); for (const pattern of SYMBOL_PATTERNS) { pattern.lastIndex = 0; let m; while ((m = pattern.exec(content)) !== null) { const name = m[1]; if (name.length >= MIN_SYMBOL_LENGTH && !STOP_WORDS.has(name.toLowerCase())) { symbols.add(name); } if (symbols.size >= MAX_SYMBOLS_PER_FILE) return [...symbols]; } } FLAG_PATTERN.lastIndex = 0; let m; while ((m = FLAG_PATTERN.exec(content)) !== null) { symbols.add(m[1]); if (symbols.size >= MAX_SYMBOLS_PER_FILE) break; } return [...symbols]; } function buildSymbolRegExp(symbols) { if (!symbols.length) return null; const alternatives = symbols .slice() .sort((a, b) => b.length - a.length) .map(escapeRegExp) .join('|'); return new RegExp('(?<![\\w$-])(' + alternatives + ')(?![\\w$])'); } function collectBacktickTokens(line) { BACKTICK_TOKEN.lastIndex = 0; const tokens = []; let m; while ((m = BACKTICK_TOKEN.exec(line)) !== null) tokens.push(m[1]); return tokens; } // path-like means a concrete repo-relative file name: not a glob, not a // phrase, not an absolute path or slash command, and not a URL fragment or // directory mention, which is why a file extension is required function looksLikePath(token) { if (/[\s*?{}]/.test(token) || token.startsWith('/')) return false; if (!/\.[A-Za-z][A-Za-z0-9]{0,7}$/.test(token)) return false; return token.in
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withstale-docs
Your docs can never lie again. You change code and the README quietly stops being true. Nobody notices until a stranger does.
Get the whole plugin
Stats
3
Stars
1
Forks
Maintained
Maintenance
JavaScript
Language
MIT
License
1mo ago
Last commit
2mo ago
Created
Repo: SectionTN/stale-docs

