A compact symbol index, markdown section maps and task tags, so Claude reads the index and a few lines instead of whole files.

A Claude Code plugin that helps Claude read a codebase without reading whole files. It builds a compact index of every class, function and method (with its line and the comment above it) and of every markdown heading (with its line range), so Claude can rg the index, then read only the lines it needs.
It ships:
scripts/build_code_index.py: the index builder. Regex based, so it needs no compiler or language server: Python, TypeScript/JavaScript, C/C++, C#, Go, Rust, Java, Kotlin and Markdown. Incremental: unchanged files come from a cache. Markdown output by default, JSON when the index path ends in .json.scripts/md_toc.py: prints any markdown file's headings with [start-end] line ranges.scripts/status_tags.py: lists, summarises and strips STATUS(<task>) comments, one-line notes that say where a task stands at that spot in the code.code-index) that teaches the discipline: index, then rg, then ranged reads./index to rebuild it.Python 3.9+ with the standard library only.
/plugin install code-index --marketplace azoof-ahmed/claude-mods
Then run /index once in each project to build the index.
Set them in /config, or in settings under pluginConfigs["code-index"].options.
| Option | Default | What it does |
|---|---|---|
indexPath | .claude/index/code_index.md | Where the index is written, relative to the project root. .json writes JSON. |
include | **/*.{py,pyi,ts,tsx,mts,cts,js,jsx,mjs,cjs,c,h,cc,cpp,cxx,hpp,hh,hxx,inl,cs,go,rs,java,kt,kts,md} | Comma-separated globs of the files to index. |
exclude | node_modules, .git, dist, build, out, target, bin, obj, vendor, third_party, virtualenvs, __pycache__, .next, coverage, .claude, *.min.js | Comma-separated globs to skip. In a git repository .gitignore is honoured too. |
statusTag | STATUS | The tag name of task comments. |
python | python | The Python executable (python3, py, or a full path). |
The options reach the scripts as environment variables (CODE_INDEX_PATH, CODE_INDEX_INCLUDE, CODE_INDEX_EXCLUDE, CODE_INDEX_STATUS_TAG, CODE_INDEX_PYTHON, plus the script paths CODE_INDEX_CLI, CODE_INDEX_TOC, CODE_INDEX_TAGS), so a project's own settings beat them:
{ "code-index": { "include": "src/**/*.ts,docs/**/*.md", "indexPath": "docs/index/code.md" } }
in <project>/.claude/claude-mods.json. Order, highest first: a flag typed on the command line (--index-path, --include, --exclude, --status-tag) > .claude/claude-mods.json > environment > default. The project root is the nearest folder holding .git or .claude.
/index # rebuild (incremental); /index --full reparses everything
rg -n "parseConfig" .claude/index/code_index.md
python <plugin>/scripts/md_toc.py docs/DESIGN.md --grep auth
python <plugin>/scripts/status_tags.py list --task auth-42
A tag looks like // STATUS(auth-42): PARTIAL -- refresh done, revoke left [ana] (DONE, PARTIAL, OPEN, FROZEN), on its own line in the file's comment style. Strip a task's tags when it closes: status_tags.py strip --task auth-42.
claude plugin validate .
claude plugin test .
python -m unittest discover -s scripts/tests
claude --plugin-dir .
MIT
hooks/register.ts 91 lines1import type { EngineInterface, PluginOptions, Register } from 'claude-code'
2
3/** The manifest's userConfig defaults, for an option the engine did not fill in. */
4const DEFAULTS: Readonly<Record<string, string>> = {
5 indexPath: '.claude/index/code_index.md',
6 statusTag: 'STATUS',
7 python: 'python',
8}
9
10function option(options: PluginOptions, key: string): string {
11 const value = options[key]
12
13 return value === undefined || value === '' ? (DEFAULTS[key] ?? '') : String(value)
14}
15
16function script($: EngineInterface, name: string): string {
17 return `${$.plugin.root.replaceAll('\\', '/')}/scripts/${name}`
18}
19
20/** The person's options as the scripts' environment variables. They sit below a project's own
21 * `.claude/claude-mods.json` and above the scripts' defaults; only a typed flag beats the project file. */
22export function environment($: EngineInterface, options: PluginOptions): Record<string, string> {
23 return {
24 CODE_INDEX_PATH: option(options, 'indexPath'),
25 CODE_INDEX_INCLUDE: option(options, 'include'),
26 CODE_INDEX_EXCLUDE: option(options, 'exclude'),
27 CODE_INDEX_STATUS_TAG: option(options, 'statusTag'),
28 CODE_INDEX_PYTHON: option(options, 'python'),
29 CODE_INDEX_CLI: script($, 'build_code_index.py'),
30 CODE_INDEX_TOC: script($, 'md_toc.py'),
31 CODE_INDEX_TAGS: script($, 'status_tags.py'),
32 }
33}
34
35export const register: Register = (on, options) => {
36 const python = option(options, 'python')
37
38 on('session.start', async ($, e, next) => {
39 // Bash commands and the scripts they start inherit these. env.set takes string-literal names.
40 const env = environment($, options)
41 await $.env.set('CODE_INDEX_PATH', env.CODE_INDEX_PATH)
42 await $.env.set('CODE_INDEX_INCLUDE', env.CODE_INDEX_INCLUDE)
43 await $.env.set('CODE_INDEX_EXCLUDE', env.CODE_INDEX_EXCLUDE)
44 await $.env.set('CODE_INDEX_STATUS_TAG', env.CODE_INDEX_STATUS_TAG)
45 await $.env.set('CODE_INDEX_PYTHON', env.CODE_INDEX_PYTHON)
46 await $.env.set('CODE_INDEX_CLI', env.CODE_INDEX_CLI)
47 await $.env.set('CODE_INDEX_TOC', env.CODE_INDEX_TOC)
48 await $.env.set('CODE_INDEX_TAGS', env.CODE_INDEX_TAGS)
49 await $.command.register({
50 name: 'index',
51 description: 'Rebuild the code index (symbols and headings with line numbers)',
52 argumentHint: '[--full]',
53 })
54
55 return next(e)
56 })
57
58 on('command.run', { command: 'index' }, async ($, e) => {
59 const full = /(^|\s)--full(\s|$)/.test(e.args) ? ['--full'] : []
60 const ran = await $.process.run([python, script($, 'build_code_index.py'), ...full], {
61 env: environment($, options),
62 timeoutMs: 600_000,
63 })
64
65 if (ran.exitCode !== 0) {
66 return {
67 text: `${$.plugin.name}: the index build failed (exit ${ran.exitCode}).\n${(ran.stderr || ran.stdout).trim()}`,
68 }
69 }
70
71 return {
72 text: ran.stdout.trim(),
73 context: [`The code index was rebuilt: ${ran.stdout.trim()}`],
74 }
75 })
76
77 on('prompt.compose', async ($, e, next) => {
78 const composed = await next(e)
79 const text = [
80 `Code index (${$.plugin.name}): before reading source, rg the index (${option(options, 'indexPath')} unless ` +
81 "the project's .claude/claude-mods.json moves it) for the symbol, then Read only the lines around it; " +
82 'whole-file reads only for files under ~300 lines.',
83 `Rebuild it with /index or \`${python} "${script($, 'build_code_index.py')}"\`. Markdown sections with line ` +
84 `ranges: \`${python} "${script($, 'md_toc.py')}" <file.md>\`. Task tags: ` +
85 `\`${python} "${script($, 'status_tags.py')}" list [--task X]\`.`,
86 ].join('\n')
87
88 return { sections: [...composed.sections, { id: `${$.plugin.name}:commands`, text, scope: 'session' }] }
89 })
90}
91