🗂️ codemap-py — structural answers for Python codebases¶
codemap-py builds a local, static index of a Python project so maintainers can answer "what imports this?", "what calls this function?", "which tests are likely affected?", and "where is the highest coupling?" before changing code. It is useful when a task has unresolved structural scope; a fully localized edit with no such question should skip it.
The package ships the same six skills for Claude Code and Codex: scan the project, query the index, find affected tests, rename references, inspect integration, and debrief telemetry. The runtime adapters share the capability contract but keep their host-specific invocation and path rules.
Runtime notes:
- Indexing and queries run through the CLI.
.codex-mcp.jsonregisters only the local terminal-question tool (ask_user); it provides no indexing or query tools. - Codex debrief helper recipes use the quoted packaged Python launcher (
bin/python, orbin/python.cmdon native Windows), so they do not depend on a barepythoncommand on PATH. - The native question provider requires Python 3.10 or newer available as
pythonon the executable search path inherited by Codex, matching its direct MCP command. Codex Rig sync validates this prerequisite and does not install a global interpreter shim. If the host cannot start the provider, keep the required decision unresolved and use another permitted input route only when available. - Consumer plugins read the shared context and gate guidance live from the active
codemap-pyinstall; they never load private files from another plugin. The reference context batch resolves its default index from the repository root, including when invoked from a subdirectory.
Contributing or want internals? See DEVELOPMENT.md.
📋 Contents
- Quick start
- What it solves
- Adaptive use
- Prerequisites and supported runtimes
- Build and query the index
- Honest limits
- Benchmark evidence
- Six skills
- Integration with other plugins
- Configuration
- Compatibility and exit codes
- Upgrade, uninstall, and migration
- Maintainer documentation
- Contributing and feedback
⚡ Quick start¶
Install the plugin in the runtime you use:
# Claude Code
claude plugin marketplace add Borda/AI-Rig
claude plugin install codemap-py@borda-ai-rig
# OpenAI Codex
codex plugin marketplace add Borda/AI-Rig
codex plugin add codemap-py@borda-ai-rig
Start a fresh runtime session after installation. Build an index, then ask the first useful structural question:
# Claude Code
/codemap-py:scan-codebase
/codemap-py:query-code rdeps mypackage.auth
# Codex
$codemap-py:scan-codebase
$codemap-py:query-code rdeps mypackage.auth
The direct CLI is also available to a project checkout or to Claude's installed bin/ PATH. Its first query has the same shape:
From a source checkout, call the Python entrypoint instead:
python plugins/codemap-py/scripts/codemap_py_entry.py index
python plugins/codemap-py/scripts/codemap_py_entry.py query --compact rdeps mypackage.auth
python plugins/codemap-py/scripts/codemap_py_entry.py doctor --json
- Windows uses this Python entrypoint directly —
bin/codemap-pyis a#!/bin/shlauncher and does not execute there;bin/codemap-py.cmdis the installed-package equivalent. macOS and Linux may use either form. - Codex does not add the plugin's
bin/directory to PATH; use the$codemap-py:*skills or resolve the installed plugin root as their runtime instructions describe.
🎯 What it solves¶
Without a structural index, a refactor often starts with repeated file searches to discover importers, callers, and tests. That exploration can miss a reverse dependency or spend time reading files that do not answer the question. codemap-py makes those relationships queryable from one local JSON index and gives each result coverage and freshness metadata.
The index is not a replacement for reading source or running tests. It is a narrow, fast source of structural evidence that helps choose the next inspection or verification step.
- Coverage queries distinguish missing measurements from measured zero with
measurement.status(empty,unavailable,partial,available). Staticuncoveredrelationships are not measured line coverage. selectionmakes exact-module scope explicit: querying a package module does not include descendants. Enumerate and batch selected modules for package-wide work, or intentionally use--all.
Telemetry in brief¶
- CLI telemetry records handled terminal success/failure once with
exit_code, including parse, gate, and supported timeout failures. Logging is best-effort and optional; abrupt termination or an unwritable destination can leave gaps. The SIGALRM timeout capability is Unix-only. - New CLI/tool/skill records carry their originating project root. Legacy records missing project identity or terminal outcome stay untouched and counted but cannot support joins.
- Debriefs isolate recent version/project/runtime cohorts.
join_avoidance.pylabelsavoidance_countandrateasmodule_overlap_proxy_v3: verified successful complete answers followed by matching tool targets, including legitimate source reads. This is not confirmed misuse, a guard-failure rate, or measured token savings, and a changed eligibility rule makes earlier rates non-comparable. - Each overlap carries a
kind:
kind |
Meaning |
|---|---|
source_read |
Verified own-file inspection; a file scope is always this, even for a single-file search. |
structural_search |
A directory scope search. |
unknown |
Grep/Glob scope is absent or indeterminate; reported as unknown_count, overall and per runtime. |
- Bash
grep/rgrecords carrysearch_pathandsearch_scopeonly when the hook can parse a standalone command; pipelines, command lists, shell expansions, other redirections and unknown options stayunknown. - Hook-triggered
changed_countis the eligible working-tree path delta against the index commit, including unstaged and untracked sources and indexed documentation. It is not a measured reparse count; scanner exclusions apply, and invalid or unreachable revisions leave it unknown. - Explicit-root CLI telemetry and Claude marker lookup follow the resolved target project, not the caller directory.
🔗 Adaptive use¶
Use the smallest route that answers the unresolved question:
| Question | Route |
|---|---|
| Exact file and symbol are known; no structural fact remains | Skip codemap-py and edit or inspect directly |
| Which modules import a module? | rdeps <module> |
| Which modules does it import? | deps <module> |
| Which production functions directly call a function? | fn-rdeps <module::symbol> --exclude-tests |
| Which functions transitively depend on it? | fn-blast <module::symbol> |
| Which modules have the highest reverse-dependency count? | central --top N --exclude-tests |
| What source slice and imports define a symbol? | symbol <name> --with-imports |
| Which tests are structurally affected? | /codemap-py:test-impact or $codemap-py:test-impact |
| Is the integration wiring and runtime evidence healthy? | /codemap-py:integration audit or $codemap-py:integration |
- For an explicit request for structural context, query even when an edit looks small.
- A lifecycle boundary such as a callback, hook, cancellation path, cleanup path, or state transfer also needs source and the named test or oracle; a complete structural result does not prove runtime behavior.
- Resolve the installed launcher once and retain its literal for the workflow.
- Independent standalone read-only queries may run concurrently only against a prepared stable index with self-heal disabled (
SCAN_NO_AUTOBUILD=1); dependent queries wait, and refresh/self-heal/index writes are serial. - There is no arbitrary total-query cap for facts required to finish; only targeted correction retries are bounded, and recurring correction failures stop with an explicit gap.
- A complete untruncated result settles its own fact, not sibling dimensions, and an independent AST/oracle read remains valid.
rdepsanddepsanswer opposite directions. Query names and paths are relative to the project being queried, not the installed plugin.- Explicit
--root <root>selects that root's canonical index. After a custom-index scan, retain the emitted index path and query with--index <emitted-index-path> --root <same-root>so index selection and path resolution refer to the same source. fn-rdepsreports incoming call edges; it does not discover inheritance or same-name override relationships. Usefind-symbol '<ClassSuffix>\.<method>$' --exclude-tests --limit 0to gather same-name override candidates, then verify ancestry and package boundaries in source.
✅ Prerequisites and supported runtimes¶
- Claude Code or Codex, depending on the runtime you are installing into.
- CPython
>=3.11,<3.15for the shipped dispatcher and launchers. The CLI checks this before importing the package and exits127when no eligible interpreter is available. SetCODEMAP_PYTHONto an eligible interpreter when PATH discovery is not enough. - Git is recommended for branch-aware freshness checks and incremental rebuilds. Outside Git, content hashes provide a fallback.
- The core scanner and query engine use the Python standard library.
coverage>=7.4is optional and is needed only forscan-index --with-coverageand thecoverage/coverage-gapqueries.
The scanner is intended for Python projects:
- It parses
.pyand.pyifiles withast.parse. A.pyimplementation takes precedence over a sibling.pyi; a stub without an implementation contributes declarations and imports but no call edges. - It also records selected Sphinx references from
.rstfiles,docs/**/*.md, and supported root configuration files for cross-reference and freshness checks. - It does not index TypeScript, Go, Rust, or other non-Python source as Python modules.
🗂️ Build and query the index¶
The canonical CLI is:
codemap-py index [--root PATH] [--incremental] [--with-coverage PATH] [--timeout N]
codemap-py query [global flags] <subcommand> ...
codemap-py doctor [--json]
codemap-py integrate {audit,plan,apply,sync,demo} ...
--incremental requires an existing v3-or-newer index. --with-coverage reads a .coverage SQLite file when the optional dependency is available. Query help is authoritative for the complete subcommand and flag list; the groups are in Query-code subcommands.
Most query results include an index block. Read it before treating a list as final:
stalereports that the index may no longer describe the working tree.query_completedescribes graph coverage for the queried direction; it does not mean a display list was not truncated.confidence,truncated, andtotal_availabledescribe result completeness. The default display limit is bounded for several list commands; use--limit 0where that command supports it when the full set matters.not_coverednames static-analysis blind spots and should remain in the final reasoning.
Index preparation and self-heal:
- Ordinary queries automatically prepare a missing canonical index and check discovered working-tree source, documentation and consumed configuration bytes for bounded incremental self-heal, including unstaged edits, additions and deletions.
- Nested Git worktrees use their own index, never an ancestor checkout's.
SCAN_NO_AUTOBUILD=1or--no-healdisables query-time preparation; explicit foreign/custom indexes are not implicitly created or rewritten.- A self-heal stands down when a writer takes the index while the query is reading it (most often another query's own heal) and answers from the index it already loaded. A refresh already running when the query started is waited out by the read lease, after which no heal is needed.
- An explicit scan is the predictable choice after a clone, a large change, a branch switch, or when a query reports stale/degraded coverage.
The shared Claude context contract runs its selected queries directly and reports partial pre-flight retrieval when any requested query fails, misses or lacks completeness metadata. Consumer gates persist successful preparation, stale continuation and explicit skips; later queries carry --no-heal after a stale choice. Consumers retain per-child batch outcomes and reuse matching fresh answers without suppressing necessary source or test-quality inspection.
🧭 Honest limits¶
The graph is static AST evidence. It can miss dynamic dispatch, hook and callback registration, string-based dispatch,
getattrlookups,importlib.import_module,__import__, and lazy-loading patterns. Import and call results therefore do not establish runtime behavior, external consumers, test pass status, or inheritance correctness.
rename-refscalls out dynamic references, cross-repository callers, ABC/Protocol overrides, as manual review items. Both hosts preflight every known caller regardless of caller count; missing, ambiguous, or stale sites stop before source changes, and known callers are never silently skipped.- Tracked module moves preserve uncommitted edits; non-git or untracked sources and conflicted or occupied destinations stop before edits.
- Files that fail to parse are marked degraded rather than silently treated as complete. Untracked files, root mismatches, collisions, stale hashes, and list caps can all reduce confidence. Review the returned coverage metadata and inspect the relevant source and tests before making a behavior or deletion decision.
- Performance and token use vary with repository size, model, index freshness, query choice, and whether an agent continues exploring after a result. Benchmark runs are exploratory and repository/model-specific; they do not establish universal savings or quality guarantees. See the benchmark record for methods and caveats.
- Possible future work (broader dynamic-behavior evidence, deeper cross-language support, richer freshness diagnostics) is opportunity, not promise; the current contract remains static Python analysis with explicit coverage metadata.
Hooks and launchers¶
Hooks are optional; they are not required for indexing or querying.
| Hook | Runtime | Purpose |
|---|---|---|
seed-session.py |
Claude | Seeds session state at start. |
inject-preamble.py |
Both | Ambient index-status preamble and bounded refresh scheduling. |
guard-redundant-scan.py |
Both | Narrow guard against redundant import greps. |
log-skill-start.py |
Claude | Skill-start telemetry records. |
log-tool-use.py |
Both | Runtime-scoped tool records. |
record-exhausted.py |
Both | Records exhausted-query state for the gate. |
Registrations: claude-hooks.json, codex-hooks.json.
- Claude hooks provide ambient index status, session-sharded telemetry, skill-start records, and a narrow redundant-import-grep guard. Hook commands invoke the packaged launcher with
--run-hookand a supported hook basename; the launcher verifies a supported CPython runtime before dispatch. - Codex hooks cover ambient preamble/guard behavior and runtime-scoped tool records. The Codex host has no Codemap skill-start hook. The runtime difference is an evidence boundary, not a query-engine difference.
- Missing index: the shared prompt hook stays silent when scanner exclusions leave no indexable
.pyor.pyisources. Its automatic-query directive keeps the bounded package marker condition (__init__.py,pyproject.toml, orsetup.py), so marker-free scripts receive no ambient directive and need a manualcodemap-py index. An existing index still refreshes after the final source deletion. - Preamble: the status preamble tells agents to reuse an earlier answer to the same query rather than re-running it, and to keep
--topsmall when output enters context. - Refresh suppression: keyed on HEAD and actual discovered source/documentation and consumed configuration bytes, so unstaged edits and eligible untracked files schedule a new refresh; a published fingerprint suppresses repeated scans only while those inputs stay unchanged. Frozen environments retain
SCAN_NO_AUTOBUILD=1. - Reminders: prompt status and missing-index sentinels are scoped per runtime and session (falling back to
shared). After the first full stale preamble, an unchanged index SHA and currency suppress reminders for 30 minutes; a changed state emits one line and restarts the window; an expired window re-emits the full preamble; a current index clears the stale record. - An incremental scan records current source fingerprints and HEAD as
git_sha, including when no graph rebuild is needed. - Hooks suppress expected parsing and filesystem failures; this is not a guarantee against every malformed event. Skill/tool logging expects mapping-shaped
tool_inputwhen that field is truthy.
Launchers:
- Plugin
bin/pythonand native Windowsbin/python.cmdpreserve the shippedpythoncommand and probe candidate runtimes for Python 3.10+ before executing the workload once. Pluginbin/follows system directories on PATH, so an eligible systempythontakes precedence; missing eligible runtimes produce a clear diagnostic. - Hook launchers select a verified Python 3.11–3.14 runtime and pass
--run-hookonly for supported hook basenames. Linux, macOS and native Windows use the same closed dispatch contract.
📈 Benchmark evidence¶
Headline claim, bounded: on structural questions with unresolved dependency or caller scope, a required Codemap Skill reduced context and elapsed time in every model stratum measured so far, and improved the aggregate semantic answer score in five of the six. The sixth, the 2026-09-07 Codex gpt-5.6-sol stratum, is a quality tie against a control already at a median score of 1.000 — the cost reduction reproduced there and the quality gain did not. These are historical scored-cell summaries, not an all-assigned pass rate; the record retains unfavorable and unobserved cells.
Headline results, 45-task structural cohort shared by both providers, C_strict (required Skill) against the no-Codemap baseline, measured 2026-09-06:
| Provider | Model | Paired n | Baseline accuracy | Treatment accuracy | Gain | Input tokens | Elapsed |
|---|---|---|---|---|---|---|---|
| Claude | Haiku | 39 | 71.8% (28/39) | 87.2% (34/39) | +15.4 pp (+6) | −80% | −74% |
| Claude | Sonnet | 42 | 83.3% (35/42) | 92.9% (39/42) | +9.5 pp (+4) | −57% | −53% |
| Claude | Opus | 43 | 88.4% (38/43) | 97.7% (42/43) | +9.3 pp (+4) | −19% | −31% |
| Codex | Luna | 43 | 88.4% (38/43) | 97.7% (42/43) | +9.3 pp (+4) | −29% | −36% |
| Codex | Terra | 40 | 87.5% (35/40) | 92.5% (37/40) | +5.0 pp (+2) | −42% | −51% |
| Codex | Sol | 45 | 93.3% (42/45) | 93.3% (42/45) | 0.0 pp (+0) | −51% | −49% |
How to read it:
- Input tokens and elapsed are per-task medians of treatment ÷ baseline restated as change; Codex input tokens are gross, mostly cached, and the Codex runner records no per-cell cost.
- Every stratum is one study at one repetition per cell, never a Codex or Claude average. Every row has some tasks where the Codemap arm cost more: reading less is a distributional result, never a per-task guarantee.
- Optional use (
B_auto) is not reliable. On Codex it regressed on agentic import-graph tasks under prompts that left the answer population implicit, and splitting by whether a cell actually queried Codemap changed nothing on the structural cohort. Required use of the Skill is what produced the effect. - A fully localized edit with no unresolved structural fact is not a Codemap workload: single- and multi-file Fix stages cost 15–45% more input at identical correctness.
- Agentic blast-radius tasks (16 shared tasks): every required-Skill cell reports full recall and no baseline does; the Codex accuracy gains there were measured under prompts that did not state the answer population, and a re-run with explicit populations inverted the Luna accuracy half while the output-token and latency savings held.
- Extended suite (20 graph-query tasks, six model studies, 2026-09-08–09): optional Codemap raised Claude Haiku semantic credit from 77.9% to 97.3% with whole-arm cost down 46.4%; Codex required use cut median output tokens (−15% to −41%) and elapsed time (−9% to −34%) on correct-answer pairs while gross input rose. One repetition on one graph-centric repository limits generalization.
- Known open product issue: filtered centrality output can fall back to an unfiltered count for a module with only test importers.
Full per-cohort narrative: DEVELOPMENT.md. Full cohorts, estimators, per-provider snapshots, distributions, and caveats: benchmark results.
🔧 Six skills¶
Both runtimes expose these names:
| Skill | Purpose |
|---|---|
scan-codebase · Codex |
Build or refresh the Python structural index. Explicit invocation; it does not answer a query. |
query-code · Codex |
Select and render a structural query without rebuilding or editing source. |
test-impact · Codex |
Identify affected test files and emit a pytest command; it does not run tests. |
rename-refs · Codex |
Rename a Python symbol or module using static caller/import evidence, with confirmation and caveats. |
integration · Codex |
Audit, plan, apply, sync, or demo the supported consumer wiring and runtime evidence. |
debrief-coding · Codex |
Analyze local cross-runtime Codemap telemetry, optionally producing an anonymized report. |
Claude uses /codemap-py:<skill>. Codex uses $codemap-py:<skill>. The shared contract is shared/capability-contract.md; it is the authority for exit codes, completeness metadata, and static-analysis caveats.
What each skill does not do:
| Skill | It does not do |
|---|---|
scan-codebase |
Answer a query or validate runtime behavior. |
query-code |
Rename symbols, rebuild explicitly requested indexes, or replace source/test verification. |
test-impact |
Prove tests pass or resolve dynamic dispatch invisible to the static graph. |
rename-refs |
Guarantee dynamic, cross-repository, or inheritance references are covered. |
integration |
Mutate remote services, global instructions, or an installed cache directly. |
debrief-coding |
Build/query the index or validate installation health. |
Codex questions:
- Codex asks only for a genuinely missing decision, material input or protected action. Authorized preparation, internal recovery and completion need no new consent.
- Questions prefer permitted synchronous input, then this plugin's packaged native terminal form (
ask_user). The form waits for the answer, binds it to the decision ID and frozen scope digest, and does not emit a second assistant question message. Closed choices use a selectable list; open-ended indexes, ranges, severity combinations and exact-token confirmations use native text input with the complete grammar. - The form performs no commands, repository writes, credential reads, network access or permission changes. No answer, cancellation or unsupported input grants consent. Explicit Approve/Deny replies bind to one unchanged, unambiguous conversational decision; ambiguity, supersession, exact-token/digest and runtime-permission rules remain enforced.
- When plain text is required, all context, reports, confidence and limitations come before the question; the question and complete options or accepted input syntax are the final terminal block, with no trailing summary or status. The fallback is delivered once, in the final response.
- A dismissed question stays pending; the root checks another suitable control, then asks once in plain chat if none remains usable.
- Headless hosts retain an explicit unresolved-input fallback. Claude-specific question behavior is unchanged.
- Each plugin ships its own provider; no sibling installation or user-settings change is required. Host restrictions still apply, and this does not override a plain-text-only host. Details: short shared guide.
🔗 Integration with other plugins¶
The integration engine has an explicit closed consumer set:
- Claude consumers:
foundry,oss,develop, andresearch. - Codex consumer:
codex-rig.
/codemap-py:integration audit or $codemap-py:integration reports observed installed versions, roots, protocol compatibility, managed blocks, runtime-scoped telemetry, and wiring state without guessing at unavailable runtime facts.
Shared contracts are read live, not copied:
- Claude consumers load
claude-skills/_shared/codemap-gates.md(missing/stale-index gates,LAZY_CODEMAP) andclaude-skills/_shared/codemap-context.md(query mechanics, evidence lines, batch pre-flight) from the activecodemap-pyinstall, resolved through each consumer's ownresolve_shared_path.py codemap-py claude-skills/_shared. - The install record picks the version; the resolver's newest-cache tier is a degraded fallback used only when no usable install record exists or the recorded install lacks the subdir.
- Upgrading
codemap-pyupgrades the contract everywhere at once. - Without the plugin installed, every consumer prints its own "contract absent — use fallback below" line and continues; a
codemap-pyCLI onPATHwithout the plugin install is treated the same way.
Consumer integrations treat Codemap as optional structural context:
- Missing canonical index: normal queries prepare it automatically; explicit frozen mode retains a structured missing-index error.
- Stale index: normal queries attempt bounded refresh; retain any remaining stale-data caveat and targeted source inspection. Explicit opt-outs remain controlling.
- Unavailable launcher: use the host's normal source exploration rather than silently claiming that no callers or tests exist.
Integration modes:
auditis the bounded read-only route. It reports observed provider and consumer versions, managed blocks, index identity, runtime-scoped logs, usage, findings, bounded provider content identity, same-version content drift, and an explicitsession_catalog: unobservablestate when the native listing has no session provenance.planwrites an inspectable candidate and SHA-256.applychanges only an approved managed block in checked-in consumer source.syncinstalls only an approved local candidate or immutable release through the native runtime CLI.demorecords audit evidence plus one representative structural smoke query; it does not claim a plain-vs-structural comparison, token savings, or current-session activation.- Both mutation modes require the plan SHA-256 and never push Git, publish a release, edit installed caches directly, or write global Codex instructions.
- Audit reports provider identity and active-guidance reachability/content separately; it cannot claim live fresh-session activation or semantic currency from hashes alone. After a runtime sync, follow the host's fresh-session guidance. For Codex,
shared/codemap-py-integration.mdis metadata-only; the active consumer contract owns launcher validation and query guidance.
Complete integration mode reference¶
The integration skill is a thin, source-owned adapter over codemap-py integrate. It has a closed consumer set (above); it does not discover arbitrary plugins or invoke another runtime's model.
| Mode | Supported arguments | Writes or mutates |
|---|---|---|
audit |
[--runtime {claude,codex,both}] [--json] [--since YYYY-MM-DD] |
Nothing; reports observed provider/consumer/index/log evidence, findings, and remediation. |
plan |
[--runtime ...] [--consumers <csv>] [--source {local-candidate,release}] [--out <artifact>] |
A reviewable plan artifact containing targets, hashes, argv, and rollback identities. |
apply |
--plan <artifact> --approve <sha256> |
Approved managed blocks in checked-in consumer source only. |
sync |
--source {local-candidate,release} --plan <artifact> --approve <sha256> [--runtime ...] |
Approved local runtime plugin state through the native runtime CLI. |
demo |
[--runtime {claude,codex,both}] |
Audit plus one structural smoke query; disposable evidence only. |
An unrecognized mode prints the supported modes (audit, plan, apply, sync, demo) and stops; it does not open a menu. Exit codes are read per mode: for audit and demo, 1 is a fail verdict; for apply, drift or a filesystem failure; for sync, a partial result or an incomplete journal.
audit contract:
- Defaults to
both; supports--runtime claude|codex|both, JSON schema 2 (codemap-py.integration.v2), and--since YYYY-MM-DD. - Reports
pass,warn, orfail; exits0,1, or2for completed status/syntax semantics. - Records stable findings such as
runtime_log_isolation_bypassed,runtime_identity_missing,runtime_logs_not_observed,managed_block_invalid,split_index_roots,index_stale_or_unknown, andindex_degraded. - Remediation values are advisory (
plan_apply,plan_sync,source_maintenance,provider_release_required,scan_codebase,observe_next_session,none) and are never executable artifacts. --runtime claudescopes to the four Claude consumers;--runtime codexscopes tocodex-rig.--approveis valid only withapplyorsync, a saved plan, and the exact SHA-256 printed for that plan.- Active consumer guidance is checked separately from provider metadata. Missing, unreachable, or outdated guidance is reported as source maintenance or an approved
plan_syncaction; static references prove reachability only, not semantic loading or current-session activation.
Mutation boundaries:
planis non-mutating.applyrefuses path escapes, symlinks, installed-cache roots, dirty overlap, foreign markers, and body hashes that were not generated by the engine.syncuses either a deterministic local candidate or an immutable release identity and reports a journal for partial failure.- Both mutation modes leave Git commits and pushes to the maintainer.
/codemap-py:integration audit --runtime both --json
/codemap-py:integration plan --consumers foundry,oss --out .reports/integrate/plan.json
/codemap-py:integration apply --plan .reports/integrate/plan.json --approve <printed-sha256>
/codemap-py:integration sync --source release --plan .reports/integrate/plan.json --approve <printed-sha256> --runtime codex
/codemap-py:integration demo --runtime both
Scan-codebase flags, performance, and exclusions¶
The scan skill dispatches codemap-py index; it does not answer a query.
| Flag | Contract |
|---|---|
--root PATH |
Scan the selected project root instead of the Git root or current directory. |
--incremental |
Re-parse changed files against an existing v3-or-newer index; without one, fall back to a full scan. |
--with-coverage PATH |
Attach per-symbol line coverage from a .coverage SQLite file when coverage>=7.4 is installed. |
--timeout N |
Set a hard Unix SIGALRM timeout in seconds; 0 means no limit and Windows does not provide this signal. |
- The full scan walks Python files with
ast.parse, records imports, symbols, calls, hashes, source-root metadata, and degraded-file reasons, then writes one JSON index. - Incremental mode compares stored Git blob hashes or non-Git content hashes.
setup_scan_env.pyvalidates the arguments before any scan: an unknown flag or--rootwithout a value stops with the message printed unchanged, while a quoted value such as--root "a --b"is accepted. A failure parsing the arguments stops the run instead of starting a default scan. The index path is derived from the scan root (<root>/.cache/codemap), or from the Git root when--rootis absent or..- Build duration varies with repository size and filesystem; the scanner reports indexed and degraded counts, so no fixed timings are promised.
- Run a full scan after clone or a large structural change, and use incremental mode after smaller changes or when a currency gate requests it.
/codemap-py:scan-codebase
/codemap-py:scan-codebase --incremental
/codemap-py:scan-codebase --root services/api
codemap-py index --with-coverage .coverage
Built-in pruning excludes .git, virtual environments, build/dist/cache trees, node_modules, scratch/report directories, and dot-directories. Add project-specific exclusions in either form:
[tool.codemap]
exclude = ["vendor-copy", "generated/*.py"]
src_roots = ["packages/core/src", "services/api/src"]
- A bare name prunes matching directories anywhere; a path or glob matches a project-relative file path.
- Exclusions are recorded in
excluded_rootsand do not trigger incremental rebuilds. src_rootsis ordered: the first matching root determines module naming and collision priority.- The index records effective roots and any deterministic module-name collisions so a result is not mistaken for a complete graph.
Query-code subcommands and completeness contract¶
The query CLI reads an existing index and emits JSON. Feature-gated commands report that an older index must be rebuilt rather than silently returning an incomplete answer.
| Group | Commands and purpose |
|---|---|
| Modules | deps <module>, rdeps <module> [--limit N], path <from> <to>, central [--top N], coupled [--top N], list [--limit N], packages |
| Symbols | symbol <name> [--limit N] [--exclude-tests] [--with-imports], symbols <module>, find-symbol <regex> [--limit N] [--exclude-tests] |
| Calls | fn-deps <module::symbol>, fn-rdeps <module::symbol> [--exclude-tests], fn-central [--top N] [--exclude-tests], fn-blast <module::symbol> |
| Tests and edges | test-impact <module[::symbol]> [--no-mocks], mock-rdeps <module[::symbol]>, fixture-rdeps <fixture>, fixture-graph <test-file>, subprocess-deps <module>, subprocess-rdeps <module> |
| Coverage and docs | coverage <module[::symbol]>, coverage-gap [module] [--all] [--threshold P], uncovered [module] [--all] [--sort loc, name, or module] [--top N], undocumented [module] [--all] |
| Cross-references and dead code | xrefs <symbol-or-module> [--broken], dead-symbols [--min-loc N], dead-modules |
| Composite | diff-impact [--base REF] [--diff-file PATH], batch [JSON-ARRAY or JSON-PATH or stdin] |
Every query accepts these global flags before or after the subcommand:
--index PATHselects the index file.--root PATHresolves paths only; it does not retarget or rebuild an index, and a mismatch forcesquery_complete=false.--timeout Nsets the query timeout.--no-healanswers from the existing index without bounded query-time refresh.--verbose-coveragekeeps the full coverage block on every query.--compactreduces repeated coverage metadata.--format tsvwrites a table result as a header line plus tab-separated rows; the metadata envelope moves to stderr. JSON remains the default. It is refused for any result that is not a single table of flat, uniform records, so a nested value is never flattened into an unparsable cell. The batch pre-flight requests it forcentral,coupled,fn-rdepsandfn-blast;rdeps,test-impactandsymbolstay on JSON.
Query contracts:
- The module group also includes
import-types <module>. depsaccepts--stdlib,--third-party, or--internal;rdeps,central, andcoupledaccept--entity TYPEfor indexed project, test, docs, or example entities.rdeps --limit Npreviews staticimported_byonly;dynamic_imported_byandconfig_refsremain exhaustive. Defaultrdepsandrdeps --limit 0return every static importer.pathreturns exit 0 with a null path and reasonno-import-pathwhen known modules are disconnected; unknown-module and filesystem failures remain errors.- Symbol responses expose
staleandstale_reasonwhen recorded line ranges no longer match source. - Function results carry call-edge resolution (
import,local,self,star, orunresolved);fn-rdepsreports distinct caller count, not raw call-site multiplicity. fn-deps,fn-rdeps,fn-blast, and function-leveltest-impactacceptmodule::symbol, a dottedmodule.symbol(split at the longest indexed module prefix), or a bare function orClass.methodname defined once in the index. A rewritten target reports the caller's spelling innormalized_from.- A name matching several symbols, a bare method name with no class (
openagainstBox.open), or a name that is also the last segment of an indexed module (clibesidetools.cli) exits with acandidateslist instead of guessing. test-impactstill treats an exact indexed module name as module-level impact.- Every target-resolution failure adds
rejected_target(the caller's target as supplied) to its JSON error beside the unchanged exit code. Exit 1 also covers provider-side failures such as an invalid index or a disabled feature, so a consumer tells "this target does not resolve" from "this provider cannot answer" by that key, never by the message text. - The scanner records
Foo()as a call toFoo, so aFoo.__init__target also counts everyFoo()call site, in direct callers and at each step offn-blastandtest-impactwalks. Such payloads carryconstructor_callers_merged: trueand addinherited-constructorstonot_covered, because the index records no class bases and nosuper().__init__()edge. - A method or constructor with zero static callers returns a
hintfromfn-rdeps,fn-blast, and function-leveltest-impact: instance calls (obj.method()), property reads, bound-method references, and dynamic dispatch are not resolved statically, and a reference search is suggested (grep -rnE "\.method\b", or"\bClass\b"for a constructor). - A protocol (dunder) method such as
__len__or__eq__gets no search: Python calls it implicitly, so zero references is expected and never means unused, and its hint opens with "Never delete". - A
test*method in a test module gets no hint, since the test runner calls it. - The hint is advisory and does not change
query_complete; a complete answer that carries it no longer claims grep verification is not needed.
Direction and scope rules:
rdepsmeans importers;depsmeans imports.fn-rdepsmeans direct callers;fn-blastmeans transitive callers — use it only for an explicitly transitive request.centralranks reverse imports;coupledranks internal import count.- To order or threshold modules a previous query returned, pass them to
central --among <a,b,c>rather than filtering a repository-wide ranking by hand.unmatchednames every requested module the ranking did not cover, and a scoped ranking returns every candidate unless--topis given. rdepsreportsimporter_count, andexcluded_test_importer_countalongside it under--exclude-tests, so both totals come from one call; never derive one by subtracting a second call from the first.- For direct test-module importers, use
rdepsand filter test modules; reservetest-impactfor transitive affected-test selection. - Use
symbol --with-importsfor a source slice plus its module imports,find-symbolfor a regex, andpathfor the shortest import chain.find-symbolname matches are override candidates, not inheritance proof.
Batch and diff behavior:
- Batch input is a JSON array of objects such as
[{"cmd":"rdeps","args":["mypackage.auth"]}], passed inline as the argument, as a path to a file holding one, or on stdin via-(the default). - Items execute in one process and share one index load; nested
batchanddiff-impactitems are rejected. Each item retains its ownresult.indexcompleteness, truncation, total, and scope metadata, and per-item failures remain on that item; the top-level index is a conservative summary and cannot upgrade an item. Skills keep standalone queries as the default. diff-impactderives changed modules, per-module reverse dependencies/coupling, function callers, and a union of affected tests from a Git ref or unified diff.- A C-quoted path (git's default for a non-ASCII name) is unquoted before it is mapped, and Git listings are read NUL-separated, so a non-ASCII module is neither dropped nor reported stale forever.
- A
--diff-filemaps only its added lines, so a default three-context-line diff names the same changed symbols as the zero-context git path; lines split on newline only. - A changed method or constructor with zero static callers keeps the
fn-rdepshinton itsfn_rdepsentry; the payload then adds a top-levelhintstating how many there are and naming up to five, and lists the call graph'snot_coveredblind spots. A changed test method carries none of these.
Coverage metadata is intentionally dieted after the first query in a process:
- Use
--verbose-coveragewhen each result must carry the full block. query_completeis direction-scoped graph coverage, not a guarantee that a list is untruncated; readconfidence,truncated, andtotal_available.symbol,find-symbol, and list-like commands default to bounded output and accept--limit 0where documented.- A complete graph with a capped display is still only a displayed slice; a truncated
rdepspreview never settles exhaustive callers.
Interpretation sequence for the index block:
- Query first; do not spend a call on an unconditional pre-scan or freshness probe.
- Read
query_completeas direction-scoped graph coverage. - Inspect
confidence,truncated, andtotal_available; use--limit 0where supported when the complete list matters. - After a complete, untruncated result, do not re-query, read, or grep for the same structural fact; sibling queries for distinct dimensions still run when required. Source-body reads remain valid for distinct implementation or runtime details and explicit independent AST/oracle work.
- For an incomplete or degraded result, use only a targeted fallback for the named gap (
stale,degraded,not_covered, or similar); usetest-impactwhen the open question is test choice.
stale, degraded modules, untracked files, root mismatches, and collisions lower confidence and require source/test review.
codemap-py query --compact rdeps mypackage.auth --exclude-tests
codemap-py query fn-rdeps mypackage.auth::validate --exclude-tests
codemap-py query symbol validate --with-imports --limit 0
codemap-py query find-symbol '^Auth.*Handler$' --exclude-tests --limit 0
codemap-py query batch - < requests.json
Test impact and rename-refs contracts¶
test-impact:
- Accepts exactly one target, either a bare module or
module::symbol(dottedmodule.symboland bare names defined once are normalized tomodule::symbol; a module-name suffix or bare method name exits with candidates), plus--no-mocks. For more than one target, use separate invocations; the skill warns rather than silently combining them. - Tokenizes the arguments once:
--no-mocksis an exact token in any position, the first non-flag token is the target, and an unknown or flag-shaped argument is rejected. With no target the skill asks for one. - Identifies structurally affected test files, preserves the index completeness and
not_coveredcaveats, and emits a pytest command for the maintainer to review and run. Paths are quoted for POSIX shells on POSIX hosts or PowerShell on native Windows;test_filesremains the authoritative argv list for other shells or direct subprocess execution. - A target that does not resolve prints the engine's candidates or suggestions without index-rebuild advice; rebuild advice is reserved for provider failures such as a missing, oversized or corrupt index. The report lists the test files,
via_callandvia_mockcounts, the pytest command and the completeness signals exactly as the helper prints them; index builds run with--timeout 360. - Does not execute tests, find every caller, or prove runtime behavior.
rename-refs has two explicit subcommands:
/codemap-py:rename-refs symbol <old-qname> <new-qname> [--dry-run] [--deprecate[=<decorator>] | --no-deprecate] [--since <version>] [--removed-in <version>] [--remove-if-no-callers]
/codemap-py:rename-refs module <old-module> <new-module> [--dry-run]
- Scope: both hosts check for Python sources from the repository root, excluding Git metadata, virtual environments and
node_modules. - Symbol route: updates a one-to-one Python definition and statically visible references. Call edges alone miss most references to a class or callback, so it also edits non-call references — imports, base classes,
isinstancetargets, callback values,__all__strings and ancestor__init__.pyre-exports — in every file that imports the symbol's module or an ancestor package (rdeps). A same-name symbol imported from another module is left alone. Callers reached throughmodule::Class.methodare resolved by exact qualified name. - Module route: renames a file/module and its import lines, indented ones inside functions,
tryorif TYPE_CHECKING:blocks included, plus attribute uses such asmypackage.old.func()afterimport mypackage. It derives its import root from the whole relative path, sosrc/layouts rename correctly; a path that does not match the indexed file stops. The blast report also prints the index's advisory search for references outside the static AST, such as hook-registered callers. --dry-runpreviews without editing.--deprecateis symbol-only and defaults to the project deprecation decorator;--sinceand--removed-inadd the version window. Quote a decorator value with single quotes (--deprecate='@deprecated_class(...)'): the Claude skill pastes its arguments into a double-quoted shell word, so a double quote ends it.--no-deprecatedeclines the wrapper explicitly and does not conflict with--remove-if-no-callers.- Validation before any edit: arguments are tokenized once; unknown flags, flags missing a value, leftover tokens, references that are not dotted identifiers, and symbol-only flags on
moduleare rejected, and::input tomodulegets its own message. Afn-rdeps,rdepsorfind-symbolerror stops the run before any edit. Caller sets and the--remove-if-no-callersdelete gate also require a whole-word search that finds no non-definition reference, in dry-run and apply alike. --remove-if-no-callersis a hard safety gate: honored only when the caller graph is complete, zero callers are found, and the user confirms removal. A method or constructor whose zero-caller answer carries ahintis refused: instance calls, overrides, and subclass constructors are never resolved statically. Run the hint's search and delete manually if it finds no reference; a protocol (dunder) method is never deleted on zero callers.- Refusals: ambiguous or one-to-many matches, stale or degraded coverage, path escapes, dynamic references, cross-repository callers, and caller sets above the edit cap; inspect source and tests for those cases.
- Stops: a stop before the first source write ends the invocation. A stop after a write halts further rename edits, finishes available verification, and reaches the
⚠ Rename incompletesummary with changed files and remaining work. Neither host finishes a stopped rename by hand. A failed re-scan stays visible in the summary and unavailable verification prevents success. - Verification: a successful edit is followed by an explicit rescan. For a symbol, it counts every whole-word reference to the old name still in
*.py/*.pyifiles (a method: only the files that call, define or import it; a--deprecatealias stub is not counted). Any unedited reference reportsRename incompletewith itsfile:line; the success line always states the residual count, so a rename never reports success while leaving anImportErrorbehind.
/codemap-py:rename-refs symbol mypackage.auth::validate mypackage.auth::verify --dry-run
/codemap-py:rename-refs symbol mypackage.auth::validate mypackage.auth::verify --deprecate --since 2.1 --removed-in 3.0
/codemap-py:rename-refs module mypackage.old_utils mypackage.utils
Debrief-coding telemetry and anonymization¶
debrief-coding reads local JSONL telemetry and writes a diagnostic report; it does not build or query the index.
- Flags:
--since YYYY-MM-DD,--session ID,--anonymize, and--output PATH(default.reports/codemap/debrief-<date>.md; an existing report is never overwritten, a-2,-3counter is appended). - Claude records CLI, skill, and tool layers under
.cache/codemap/logs/. Codex hooks record runtime-scoped CLI and tool shards but have no skill-start hook, so skill telemetry and some cross-layer joins can be unavailable. debrief_stats.pystreams every shard under the log root the hooks wrote (Git top level,CODEMAP_LOG_DIRhonoured), so a run from a subdirectory reads the same tree; the report is written from its single JSON object, not from hand-read shards.--sinceand--sessionfilter before anonymization.benchrecords, empty-command CLI records and records without a version are excluded and counted; the newest version is the primary cohort and older cohorts are never pooled.- Exit
1when no telemetry exists; exit3whenanonymize.pyorjoin_avoidance.pyruns past its 50-second cap. - Flat legacy records remain unattributed.
- Reports include overall, per-runtime, and unattributed usage summaries, refresh provenance, timing, completeness, and repeated-search avoidance. An avoidance event means a search/read names a module that a complete Codemap query already answered, not that the agent's runtime answer is incorrect;
join_avoidance.pyjoins offline with a bounded time window. token_measurementis unavailable because host hooks provide no token usage. Debrief does not measure token savings or verify live fresh-session activation.- Set
CODEMAP_LOGGING=falseto disable logging. Logs rotate at the implementation-defined size limit.
Anonymization:
--anonymizepseudonymizes qualified names with a project-local salt at.cache/codemap/logs/.salt, scrubs qualified names inside error and stderr text, hashesnot_coveredvalues, and writes export JSONL separately into a run-scoped.cache/codemap/export/<UTC stamp>/directory, so earlier exports are never read.- An existing salt must be a regular, owner-only file of exactly 32 hex bytes; a symlinked or loosely permissioned salt stops the export with exit 1.
- Never share the salt with an anonymized export.
- Anonymization protects names in the supported log fields; it is not a guarantee that arbitrary free text contains no identifying information.
/codemap-py:debrief-coding
/codemap-py:debrief-coding --since 2026-08-01 --session <session-id>
/codemap-py:debrief-coding --anonymize --output .reports/codemap/debrief-shareable.md
Scanner, query, and index architecture¶
Plugin architecture — animated diagram of the scanner, the index cache, the query surface and the skill adapters.
- The scanner is dependency-free Python: it walks the selected root, parses
.pyand.pyifiles with the standard-library AST, resolves imports and selected call edges, computes module/function graph counts, captures supported documentation references, and writes an atomic versioned JSON index. - Parse or encoding failures are retained as degraded module records instead of being silently dropped.
- Incremental refreshes track changed documentation for freshness and documentation references, but only
.pyand.pyientries become module records, so documentation changes do not contaminate module degradation counts. - The query engine loads the same JSON under a read lease, performs bounded freshness checks or self-heal, dispatches one subcommand, and returns a primary result plus an
indexcoverage block (method, confidence, query completeness, truncation, totals, degraded count, stale/root-mismatch state,not_covered). - The index is a cache, not a daemon or runtime tracer; no static result proves dynamic dispatch, external consumers, inheritance, test pass status, or behavior.
- The default index contains modules, relative paths, symbols and line ranges, direct imports, calls and resolution tags, test/entity classification, source-root metadata, exclusions, collisions, scan version, and Git blob or non-Git content hashes. The JSON format is version-gated: call-graph, fixture, subprocess, documentation, dead-code, and coverage queries refuse unsupported index versions with an upgrade/rebuild instruction.
Index locations, non-Git roots, and currency¶
- By default the index is
.cache/codemap/<project>.json, where<project>is the selected root basename. CODEMAP_INDEX_DIRchanges the base to<override>/<project>.json; use separate override directories when two projects share a basename.CODEMAP_COORDINATION_DIRmoves only the.index-rwread/write gate to the named directory and leaves the index where it resolved. Set it when the index directory cannot take writes — a read-only or shared mount, or a sandbox that grants write access to the gate alone. It names that directory itself, so give each index its own: two sharing one directory share a registry mutex and serialise against each other.--root PATHselects that source root's canonical index and resolves its paths;--index PATHexplicitly selects another admitted query file, retaining provenance and mutation boundaries. With an explicit root, the guard admits only that exact default orCODEMAP_INDEX_DIR-override emitted path outside the caller project; arbitrary sibling files remain rejected.SCAN_NO_AUTOBUILD=1disables implicit query refresh and makes a missing index a structured manual-build error.- Git repositories hash actual discovered working-tree bytes using compatible Git blob digests and retain the repository revision. Non-Git projects use content hashes, so incremental scans and stale detection still work without
git. - A custom root is recorded as
scan_root; later scans and queries must use that same tree or explicitly select the matching index. Multiple configuredsrc_rootsare ordered and recorded. - Prompt freshness runs its indexed dirty-path check from the Git root, so a nested session detects root-level
.py,.pyi,.rst, and nested documentation Markdown changes before starting its bounded refresh. - The integration and consumer currency gates prepare missing and stale indexes automatically by default.
LAZY_CODEMAPpreserves explicit confirm-first behavior;SCAN_NO_AUTOBUILD=1preserves frozen evidence. A failed preparation never becomes a fresh/available claim. - There is no post-commit hook requirement. Explicit scan remains available for CI and benchmarks where build cost should be controlled.
! BREAKING — Gate B (stale index) no longer asks before rebuilding. A consumer that reaches a stale index now runs `codemap-py index` in the foreground unprompted; a repo where a full rebuild takes minutes feels that pause where it used to get a question.
Fix: set `LAZY_CODEMAP` (any non-empty value) in the environment to restore the confirm-first prompt. Consumers reading the contract live pick the new default up as soon as this version is installed.
CODEMAP_INDEX_DIR=<absolute-cache-dir> codemap-py index --root <project-root>
SCAN_NO_AUTOBUILD=1 codemap-py query --index <matching-index> rdeps mypackage.auth
Troubleshooting¶
| Symptom | Evidence-led response |
|---|---|
127 from a launcher |
Check CODEMAP_PYTHON, PATH, and the supported CPython range before inspecting imports. |
index not found or empty results |
Confirm the selected root has Python files, check CODEMAP_INDEX_DIR, then run an explicit scan. |
stale or root_mismatch |
Re-scan the exact root and query from that root or pass the matching --index; do not report the graph as complete. |
query_complete=false |
Read completeness_reason, degraded, untracked, collision, and not_covered; investigate only the named gap. |
| capped result | Inspect truncated and total_available, then rerun with a supported larger --limit or --limit 0. |
upgrade required |
Rebuild the index with the current scanner; feature-gated graph data cannot be inferred from an older file. |
| degraded modules | Inspect the recorded path/reason; generated or syntax-invalid files remain outside reliable graph coverage. |
scan-query not found |
Use the skill, resolve the installed launcher path, or add the package bin/ directory to PATH; Codex does not inject it automatically. |
| integration missing/outdated | Run integration audit, inspect observed evidence and the source-owned managed block, then create a fresh stage-specific plan rather than editing an installed cache. |
| dynamic hook/callback/override behavior | Treat static results as candidates; inspect implementation and named tests/oracles because AST edges cannot prove runtime behavior. |
| integration drift | Run codemap-py integrate audit --json, inspect observed findings, and create a fresh stage-specific plan before applying or syncing anything. |
⚙️ Configuration¶
- The default index path is
.cache/codemap/<project>.json, where<project>is the project-root basename. SetCODEMAP_INDEX_DIRto an absolute override directory to use<override>/<project>.json; separate colliding project names with separate override directories. SCAN_NO_AUTOBUILD=1keeps query and test-impact routes from creating or refreshing an index implicitly.- Use
--root PATHwhen the Python tree is a subproject or monorepo component. The scan names the index from that root's basename, and later queries must use the same root or an explicit matching index.--rooton query controls file-path resolution; it does not retarget an index built for a different tree, and a mismatch is reported rather than silently accepted.
Permissions (Claude)¶
.claude-plugin/permissions-allow.jsonlists the tool calls the skills expect to be pre-approved..claude-plugin/permissions-deny.jsonlists operations that must stay denied no matter how broad the allow list becomes: destructive shell and git commands (rm -rf,sudo,ssh,chmod 777, force-push,claude --dangerously-skip-permissions).- It no longer denies GitHub writes made through
gh: foundry's gh write guard (gh-write-guard.js) lets each one run only after the user approves that exact command, and Claude Code still applies a settings deny rule after a hook allows a call, so a merged deny entry would defeat that approval. - Without Foundry a gh write reaches the normal permission prompt, except through the two GitHub API calls this plugin's own allow list pre-approves (
Bash(gh api repos/*),Bash(gh api graphql:*)): a field POST or a GraphQL mutation made through them runs unprompted, a known gap kept so a codemap-only install reads GitHub without prompts. - Neither file is merged into
~/.claude/settings.jsonautomatically. The siblingcc_*plugins each merge their own pair from their/<plugin>:setupskill; this plugin ships no setup skill, so the merge is manual. - On upgrade, the allow command below removes the obsolete
Bash(gh api repos/*:*)entry and the broadBash(find:*),Bash(node:*),Bash(claude:*),Bash(sed:*), andBash(eval:*)entries, including identical user-added entries; other allow entries are preserved. Ad-hoc commands in those five families now prompt unless a narrower rule covers them. A broadpythonallow remains for plugin helpers, flag parsing, andpython -mrecipes; no shipped skill runseval. - The deny merge is additive; stale gh write and local branch/tag deletion denies from earlier installs remain until you run
/foundry:setup(requiresfoundryplugin). - The merges are idempotent — the
reducededupe keeps the first occurrence of each rule, so existing entries neither duplicate nor get re-sorted:
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-plugins/codemap-py}"
[ -f ~/.claude/settings.json ] || printf '{}\n' > ~/.claude/settings.json
cp ~/.claude/settings.json ~/.claude/settings.json.bak
jq --slurpfile perms "$PLUGIN_ROOT/.claude-plugin/permissions-allow.json" \
'.permissions.allow = (((.permissions.allow // []) | map(select(. != "Bash(gh api repos/*:*)" and . != "Bash(claude:*)" and . != "Bash(find:*)" and . != "Bash(node:*)" and . != "Bash(sed:*)" and . != "Bash(eval:*)"))) + $perms[0] | reduce .[] as $x ([]; if index($x) == null then . + [$x] else . end))' \
~/.claude/settings.json > ~/.claude/settings.json.tmp && mv ~/.claude/settings.json.tmp ~/.claude/settings.json
jq --slurpfile deny "$PLUGIN_ROOT/.claude-plugin/permissions-deny.json" \
'.permissions.deny = (((.permissions.deny // []) + $deny[0]) | reduce .[] as $x ([]; if index($x) == null then . + [$x] else . end))' \
~/.claude/settings.json > ~/.claude/settings.json.tmp && mv ~/.claude/settings.json.tmp ~/.claude/settings.json
The first two lines cover a first install, where ~/.claude/settings.json may not exist yet, and leave a backup at ~/.claude/settings.json.bak; run jq empty ~/.claude/settings.json afterwards and restore from that backup if it does not parse. Deny wins over allow in Claude Code, so the order of the two merges does not matter. Deny entries are prefix matches, so they stop the documented command forms rather than every possible flag ordering.
🔢 Compatibility and exit codes¶
scan-index and scan-query remain compatibility aliases for the canonical codemap-py index and codemap-py query launchers. New skill and documentation examples use the canonical dispatcher. The .cache/codemap/ layout and CODEMAP_* variables remain compatible with the renamed product.
! BREAKING — the Claude skill namespace changed from `/codemap:*` to `/codemap-py:*`. Every saved prompt, alias, or automation invoking a `/codemap:scan-codebase`-style trigger stops resolving.
Fix: update each call site to `/codemap-py:<skill>`. `scan-index`/`scan-query`, `.cache/codemap/`, and every `CODEMAP_*` variable are unaffected and keep working unchanged.
! BREAKING — the `path` query changed its no-path result shape. A legitimate "no import path exists" answer now returns `{"path": null, "reason": "no-import-path"}` at exit 0; the former `{"error": "No import path found."}` key is gone. Any consumer branching on that `error` key silently misreads a valid empty result as a failure.
Fix: test `path === null` or read `reason`. Genuine failures (unknown module) still use the non-zero `error` contract, so the two cases are now distinguishable.
| Exit | Meaning |
|---|---|
0 |
Successful request, including a valid empty result. |
1 |
Runtime, index, filesystem, or integration failure. |
2 |
Invalid command syntax, option, or approval. |
3 |
Requested module or symbol is not indexed where the command distinguishes that case. |
127 |
No eligible CPython interpreter was found by the dispatcher. |
⬆️ Upgrade, uninstall, and migration¶
Upgrade through the runtime's normal plugin manager and start a fresh session. Run only the command for the runtime you use:
Consumer wiring is source-owned and managed through the integration contract; an upgrade does not inject files into an installed cache.
Upgrading from codemap¶
The direct successor to the old codemap plugin is codemap-py.
Do not run both identities in one session — the legacy plugin does not implement the shared-index read/write gate and is rejected as a concurrent producer.
- Note the installed
codemapversion so a rollback has a known target. - Close old sessions, then uninstall or disable the old plugin.
- Install
codemap-py. - Start a fresh session and run the runtime's integration audit.
The project cache is retained and revalidated; no migration step deletes it.
Rolling back¶
- Uninstall or disable
codemap-pyand close its sessions. - Reinstall the old
codemaprelease from the immutable rollback source — commit08e06b7a, legacycodemap0.24.1. - Start a fresh session.
- Verify the old
/codemap:*commands resolve again against the retained.cache/codemap/project cache.
Rollback never deletes or rewrites the project cache; it is only ever read and revalidated.
Uninstall¶
Run only the command for the runtime you installed into:
📚 Maintainer documentation¶
bin/README.mddocuments shipped launchers, helpers, and compatibility shims.scripts/README.mddocuments deterministic package builds, validation, and install probes.- The rendered Codemap-py page projects this README into the documentation site.
CHANGELOG.mdrecords versioned runtime and documentation changes.
📝 Related articles¶
- One Tool, Two Agents: What It Actually Costs to Support Both (CodeX, Sep 2026): the structural engine ported to a second agent platform, and where assumptions broke.
- A Structural Index Pays Off Most Where the Model Is Weakest (CodeX, Aug 2026): controlled refactoring-benchmark results on token savings and quality.
- Your Coding Agent Is Flying Blind — And IDEs Fixed This Twenty Years Ago (CodeX, Aug 2026): terminal agents regressed to grep, and a stdlib-only plugin that restores structural lookup.
- My Benchmark Lied to Me Twelve Times Before It Told the Truth (CodeX, Aug 2026): how an evaluation harness deceives its builder when comparing two coding agents.
🙏 Contributing and feedback¶
Open an issue in Borda/AI-Rig with the codemap-py version, CPython version, command, project layout, and the complete error or coverage block. Keep benchmark task IDs and repository-specific fixtures in benchmark evidence, not in shipped plugin docs. Changes to skills, hooks, manifests, or runtime contracts require synchronized README updates and the plugin checks described in the repository authoring guidance.
Python hook module docstrings describe their event inputs, local side effects, output envelopes, and failure handling. Pure helper examples run as doctests in the repository pytest suite; hook lifecycle and filesystem behavior use pytest fixtures.
Historical benchmark anchors are retained for links from the benchmark record. Current values, methods, and limitations belong in
benchmarks/README.md; this plugin README intentionally does not duplicate run-specific tables.