Skip to content
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.

From plugin
stale-docs
31 skill1 command1 hook

Claude Code

> /plugin marketplace add SectionTN/stale-docs
> /plugin install stale-docs@stale-docs

What fires, and when

PostToolUse

  • MatchesEdit|Write|MultiEditnode "${CLAUDE_PLUGIN_ROOT}/hooks/check-stale.js"
Read hooks/hooks.json

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.

@sectiontn
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.

Stars on GitHub

Momentum

stars in total
3
Star history of SectionTN/stale-docs on GitHub, a chart by star-history.com

More plugins that ship hooks.

claude-code-mastery

claude-code-mastery

Gives you a guide to setting up Claude Code, covering global instructions, MCP servers, custom commands, hooks and skills, plus tools for generating commit messages and auditing code for security issues. This version is marked obsolete, with a newer starter kit recommended instead.

2 skills+2558
@thedecipherist@thedecipherist
Install Plugin
claudeforge

claudeforge

Automatically creates, enhances and maintains CLAUDE.md files for Claude Code projects, so you don't have to write and update them by hand.

3 hooks+1434
@alirezarezvani@alirezarezvani
Install Plugin
scaccogatto-okf

scaccogatto-okf

Gives your coding agent skills to author, maintain, validate and visualize Open Knowledge Format bundles, portable markdown knowledge that both your team and your agents can read. Includes a backfill skill for building a bundle from an existing repository's git history and session transcripts.

4 skills+3430
@scaccogatto@scaccogatto
Install Plugin
reviewable-html-workbench

reviewable-html-workbe…

Turns design docs and plan drafts into HTML pages you can comment on inline. Fires when you ask to review a design, preview a plan, or render visual output.

3 skills1 hook302
@u-ichi@u-ichi
Install Plugin
filetree

filetree

A Claude Code plugin that maintains FILETREE.md, a one-line description per file with content hashes for staleness detection. Lets the LLM grasp repo layout in a few hundred tokens before touching code.

1 skill+2164
@nekocode@nekocode
Install Plugin
fluencyloop

fluencyloop

Keeps you fluent in code that AI writes: each feature is designed, built with the reasoning behind each decision explained, and recorded as docs, decisions and review summaries, so the understanding stays with the code.

6 skills1 hook47
@baokhang83@baokhang83
Install Plugin
d2

d2

A Claude Code plugin that reads your codebase and draws its infrastructure and architecture with D2, then keeps those diagrams in step with the code as it changes. Command: /d2:diagram.

1 skill+218
@heathdutton@heathdutton
Install Plugin

Questions about the hooks in stale-docs.

Which coding agents can use the hooks in stale-docs?
The hooks in stale-docs install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.