Real-time codebase architecture analysis and visualization for ProvenMap Portal. Analyzes codebases across multiple languages, detects tech stacks, identifies…

The pmap-code plugin — codebase architecture analysis for ProvenMap Portal. Discovers your project's components, classifies them against your org's archetype catalogue, maps relationships, and syncs the result as a layered architecture board you can review and share.
ProvenMap Code runs in Claude Code. It also grounds a board in a repo's documents with /ground — the ADRs and design docs beside the code, or a repo that holds only documents and grounds a board an architect authored.
Add the ProvenMap marketplace once, then install the plugin:
/plugin marketplace add provenmap/pmap-claude
/plugin install pmap-code@provenmap
Restart Claude Code so the commands load. Scope the install with --scope user (default), --scope project, or --scope local. Claude Cowork installs from the same marketplace.
pmap-codeis the install id — everywhere else it's ProvenMap Code.
Two ways to connect this repo to a board — both end with the credential pair in .provenmap/credentials.json (owner-read-only) and the settings in .provenmap/config.json:
Browser login (fastest): run /login. It signs you in through the ProvenMap portal, lets you pick a workspace and a board that already has a Code Plugin binding, and writes both files for you — no tokens to copy. Re-running /login while connected just confirms the connection; /login switch (or /configure's "change board" option) binds the project to a different board, and /logout disconnects it (clears the local credentials).
Manual: get a Binding Token and API Secret from the ProvenMap Portal (Sources → Add Source → Board Builder — saving generates the secret), then create two files under .provenmap/ at your repo root. credentials.json holds the pair (/login writes it with mode 0600; do the same by hand):
{
"bindingToken": "your-base64url-binding-token",
"apiSecret": "pmap_cp_live_your_api_secret"
}
config.json holds the settings — a credential field left here is ignored:
{
"boardSlug": "my-project-overview",
"branch": "main",
"baseUrl": "https://platform.provenmap.com/api"
}
| Field | File | Required | Notes |
|---|---|---|---|
bindingToken | credentials.json | yes | Base64url workspaceId::bindingId from the portal. Sent as X-CodePlugin-Token. |
apiSecret | credentials.json | yes | Starts with pmap_cp_live_ (pmap_cp_test_ from a non-production platform; secrets issued earlier start ck_cp_live_ and keep working). Sent as X-CodePlugin-Secret. |
boardSlug | config.json | yes | Target board. /configure can discover and write it for you. |
branch | config.json | yes | Must match the binding, or /sync returns 400 Branch Mismatch. |
baseUrl | config.json | no | Override for self-hosted (default https://platform.provenmap.com/api). |
excludePaths | config.json | no | Paths skipped during analysis (default node_modules, dist, .git, coverage). |
includeTests | config.json | no | Include test files in analysis (default false). |
includeSourceReferences | config.json | no | Attach file-path references to synced nodes/edges (default true). |
analysis.minorFiles | config.json | no | How small helper files land on drill-down boards (default all): a file of at most analysis.minorMaxLines lines that other files import and that exports no class folds into the node that uses it instead of becoming its own node. one-host folds only files with a single importer; off disables the fold. |
analysis.minorMaxLines | config.json | no | Line cap for that fold (default 100). Files above it are always their own candidates. |
Run /configure to validate both files, test the connection, and add .provenmap/ to your .gitignore. Credentials live only in credentials.json — never logged, and sent only to platform.provenmap.com.
Testing against a non-production server: set
PMAP_BASE_URL(or pass--base-urlto the CLI scripts) to point every command — including/login's device handshake — at a staging or local API. The browser login and app URLs then follow from that server's configuration, so nothing is pinned to production. A successful/loginwrites the URL it ran against into.provenmap/config.json, so the repo stays on that server without the env var; when both are set,PMAP_BASE_URLwins.
/start — the guided front door: the surfaces card, the ranked next step for this repo's real state, and a menu to run it. Every command closes with an Outcome — what it did, what it left, the next move and why — written from a script brief of this repo's real state./login (browser) or /configure (manual) — connect to the portal (one-time per repo)/analyze — full architecture analysis (incremental — only changed files re-analyzed)/sync — push the analysis to your board/ground — link the repo's ADRs, RFCs and design docs to the board's nodes (or, in a documents-only repo, mirror the architect-authored board and ground it)/insights — run server-defined analyses (security, performance, etc.) against the board/status — see what's analyzed, synced and grounded/analyze-archetypes is optional — see Archetypes.
Automatically detects projects in 7+ languages via their manifest files:
| Language | Manifest | Common frameworks |
|---|---|---|
| JS/TypeScript | package.json | Next.js, NestJS, Express, React, Angular, Vue |
| Python | requirements.txt, pyproject.toml, Pipfile | Django, FastAPI, Flask, Celery |
| Java | pom.xml, build.gradle | Spring Boot, Quarkus, Micronaut |
| Go | go.mod | Gin, Echo, Fiber, gRPC |
| C# / .NET | *.csproj, *.sln | ASP.NET Core, Blazor, EF |
| Ruby | Gemfile | Rails, Sinatra, Sidekiq |
| Rust | Cargo.toml | Actix, Axum, Rocket |
Polyglot projects produce a unified board with cross-language relationships (HTTP, gRPC, message queues).
| Command | Description | ||
|---|---|---|---|
/start | Start here — the surfaces card, the ranked next step, and a menu to run it | ||
/login | Browser sign-in: pick a workspace + bound board, writes config automatically | ||
/configure | Set up portal credentials manually, or switch to a different board | ||
/analyze [path] | Analyze codebase architecture (incremental; re-analyzes only changed files) | ||
/analyze --clean | Full re-analysis from scratch, ignoring existing board data | ||
/analyze --drill <board>/<node> | Drill into a node to produce a child layer board | ||
/analyze --all | Re-analyze every layer board in the manifest | ||
/analyze --scheduled | Unattended upkeep for a host schedule: refresh the boards whose files changed, then sync them (the recurring-runs reference in the provenmap-integration skill has the recipe) | ||
/sync [--board <slug>] | Push analysis to portal (smart diff: only changed elements) | ||
/sync --all | Push every board in the manifest | ||
/ground [--board <slug>] | Ground the board in this repo's documents: mirror the authored board (or read the analysed, pushed one), propose and push node↔document evidence links, report drift | ||
/insights | List available insight skills and run one against the current board | ||
/insights <skill-slug> | Run a specific insight skill directly | ||
/insights --all | Run every available insight skill | ||
/skills [--status] | Compile the platform's skill bundle (specs + guidelines) into the repo — never overwrites local edits | ||
/build [--plan] | Build the app from the platform's spec — compiled skills, work items, board design, aspect contracts (write-capable; --plan = plan only) | ||
/work-items | Pull architect-authored work items for the board and pick one to implement | ||
/work-items <workItemId> | Claim, implement, verify, and resolve a specific work item (write-capable — edits project files) | ||
/discover [count] [--auto] [--lens …] [--board <slug>] [focus] | Discover the insights and context boards worth showing — ranked by the graph, picked by you or chosen for you, authored in parallel, pushed to ProvenMap | ||
| `/adopt [--aspect <kind> \ | --db \ | --api]` | Extract a code aspect (database schema, API surface, frontend pages, event catalog) onto the bound board |
/monitor · /monitor setup | Correlate monitoring signals (errors, logs, cloud costs) with the board and push findings as a draft insight; setup configures sources + a recurring run | ||
/status | Show the lifecycle dial, config state, analysis summary, and per-board sync status | ||
/help | List commands grouped by lifecycle stage, with the plugin version | ||
/update | Update this plugin to the latest published version for your host | ||
/analyze-archetypes | Advanced — customize the archetype vocabulary: scan for gaps, submit proposals for admin review | ||
/analyze-archetypes --dry-run | Validate scan locally + ask server to dry-run, don't persist or POST | ||
/analyze-archetypes --skip-submit | Write the proposals file for manual review; don't POST | ||
/analyze-archetypes --replace | When submitting, send mode='replace' to overwrite pending payload |
/ground keeps a board honest against the documents that back it. It has two shapes:
/analyze built. After /sync has pushed the board, /ground reads the local board, inventories the repo's documents (ADRs, RFCs, READMEs, design docs) and proposes an evidence link wherever a document substantiates a node — anchor and quoted excerpt included. The analysed board is never overwritten./ground mirrors it locally (so /insights and /discover can read it), inventories the corpus and links the documents to its nodes. There is no /analyze phase — grounding is the whole lifecycle, and /start routes there.Either way the push replaces the binding's evidence set on the server, and a later run reports drift — a linked document changed or vanished — so a citation never goes silently stale. A run whose pull finds nothing drifted, missing or unlinked closes without proposing anything, which makes a scheduled /ground cheap (the recurring-runs reference in the provenmap-integration skill has the surfaces).
Supported document formats: Markdown/MDX (.md, .mdx, .markdown), reStructuredText (.rst), AsciiDoc (.adoc, .asciidoc), plain text (.txt), HTML wiki exports (.html), PDF (.pdf, extractable text). The walk covers the whole repo, skipping dot-directories, node_modules and any configured excludePaths.
For non-trivial codebases, /analyze produces a hierarchy of boards so you can navigate from a 10–30 node overview down to component-level detail without overwhelming any single view:
| Layer | Name | Scope | Target node count |
|---|---|---|---|
| L0 | System Context | This system's deployables + the outside systems they talk to | 10–30 |
| L1 | Domain | Domain or workspace drill-down | 10–40 per board |
| L2 | Component | Service or module drill-down | 5–20 per board |
| L3+ | Detail | Deep internals, as deep as the code demands — to L4 by default (analysis.plan.maxDepth) | 5–15 per board |
Board hierarchy and per-board sync state live in .provenmap/boards/:
manifest.json — every board's slug, layer, parent, and analysis state<board-slug>.json — analysis output (nodes + edges) for that boardstores/<board-slug>.store.json — sync state, content hashes, last pushComponents are classified using archetypes defined on your ProvenMap server, not a fixed list shipped with the plugin. /analyze fetches the current catalogue via /code-plugin/archetypes and the architecture-analyzer agent assigns each discovered node a valid archetype name.
Where your codebase has a pattern the catalogue has no good name for, /analyze types it with the closest available archetype, records the gap in the board's metadata.archetypeGaps, and names it once at the end of the run. Nothing blocks — the board is complete either way.
Acting on that is optional. /analyze-archetypes scans for the same gaps and submits proposals (new archetypes, or improvements to existing ones) for admin review; once approved, /analyze --clean retypes the affected components.
Set this in .provenmap/config.json to restore the old two-phase workflow, where /analyze stops and prompts whenever the archetype lock is missing, stale, or was skipped:
{ "analysis": { "archetypeGate": "strict" } }
Remove the key to go back to the optional default.
Open the ProvenMap UI to see the current archetype catalogue — the plugin fetches it automatically during /analyze.
Analysis output at .provenmap/boards/<board-slug>.json:
{
"metadata": {
"analyzedAt": "2026-05-13T10:30:00Z",
"analyzedAtCommit": "abc1234",
"projectName": "my-project",
"languages": ["python", "typescript"],
"techStacks": ["fastapi", "react"],
"layer": 0
},
"nodes": [
{
"slug": "user-service",
"name": "User Service",
"type": "<server-archetype-name>",
"description": "User management service",
"path": "src/services/user_service.py",
"parentSlug": "backend-domain",
"metadata": { "language": "python", "framework": "fastapi" }
}
],
"edges": [
{
"sourceSlug": "user-service",
"targetSlug": "user-repository",
"type": "<server-edge-archetype-name>",
"metadata": { "importPath": "from .repository import UserRepository" }
}
]
}
The type field holds the server archetype name assigned by the analyzer. Edge type values come from the edge-archetype list returned by the same endpoint.
| Agent | Role |
|---|---|
| architecture-analyzer | Multi-language structure analysis, archetype classification, domain grouping, hierarchy building |
| relationship-detector | Imports, DB operations, HTTP/gRPC calls, message queue patterns across languages |
/analyze orchestrates both agents. They can also be invoked directly from Claude Code's subagent picker to debug a specific analysis step.
git diff for change detection)BUSL-1.1
hooks/pmap-guard.js 188 lines1// ProvenMap guard: a Claude Code mod (hooks.json "modules"), shipped as authored.
2//
3// It enforces two rules the shipped content only states in prose, at the tool call:
4// 1. Secrets never enter the chat. No Read, Grep or shell command touches the
5// ProvenMap credential files or the PMAP_* secret variables, and any ProvenMap
6// secret in what a tool returns (a credential pasted into config.json, an MCP
7// entry in a host config) reaches Claude masked to its prefix.
8// 2. Script-owned state is written only by the scripts. No Edit, Write or shell
9// write touches the element and evidence stores, the board manifest or the
10// tree plan, whose hashes and records the scripts own.
11//
12// Claude Code only: Codex and Cursor have no mods, so there the prose is the rule.
13// A user-scope plugin's mod runs in every session, ProvenMap repo or not, so each
14// rule needs a `.provenmap/` path or a PMAP_ name before it fires. Every deny tells
15// Claude what to run instead. A rule that throws lets the call through (fails open),
16// like the settings-hook scripts; the output is masked either way (see `recover`).
17
18const SECRET_FILE =
19 /(^|\/)\.provenmap\/(credentials\.json|login-state\.json|login-state(\/|$)|architect-mcp\.json)/;
20const SECRET_ENV = /\bPMAP_(API_SECRET|BINDING_TOKEN)\b/;
21// Every ProvenMap secret names its family up front: pmap_<kind>_<env>_<body> (cp, mcp,
22// sess, share, app, …), or the older ck_[<kind>_]live_<body>. This is the platform's
23// own redaction pattern (its credential registry): loose enough for a truncated
24// secret, while a bare prefix (`pmap_cp_live_…`) or a short fixture stays readable.
25const SECRET_VALUE = /\b((?:pmap|ck)_(?:[a-z]+_)?(?:live|test)_)[0-9A-Za-z_-]{16,}/g;
26const SCRIPT_OWNED =
27 /(^|\/)\.provenmap\/(boards\/manifest\.json|boards\/stores\/[^/]+\.(store|evidence)\.json|tree-plan\.json|plan-run\.json)$/;
28
29// A Grep or shell read rooted at these directories (or a glob in them) reads the
30// credential files inside them.
31const SECRET_DIR = /(^|\/)\.provenmap(\/login-state)?\/?(\*[^/]*)?$/;
32
33// Programs that can name a credential path without printing the file.
34const SAFE_PROGRAM = /^(ls|stat|test|\[|chmod|find|mkdir|cd|touch|echo|printf|true|false)$/;
35const SECRET_NAME = /credentials\.json|login-state|architect-mcp\.json/;
36
37function normalise(p) {
38 return typeof p === "string" ? p.replace(/\\/g, "/") : "";
39}
40
41function unquote(word) {
42 return normalise(word.replace(/^["']|["']$/g, ""));
43}
44
45function shellSegments(command) {
46 return command.split(/&&|\|\||[;|\n]/);
47}
48
49function secretDeny(target) {
50 const architect = /architect-mcp\.json/.test(target);
51 return [
52 `ProvenMap guard: ${target} holds a ProvenMap credential, and credentials never enter the chat.`,
53 architect
54 ? "Run /pmap-architect:status to check the token, or /pmap-architect:login to replace it."
55 : "Run /pmap-code:configure to check the credentials (its script reports each field's shape and the masked secret), or /pmap-code:login to replace them. The user edits the file by hand, never through the chat.",
56 ].join(" ");
57}
58
59function scriptOwnedDeny(target) {
60 const store = /\.(store|evidence)\.json$/.test(target);
61 return [
62 `ProvenMap guard: ${target} is written only by the ProvenMap scripts, and a hand edit breaks the record they verify.`,
63 store
64 ? "Run /pmap-code:sync to update the element store, or /pmap-code:ground for the evidence store."
65 : "Change the board files instead and let the scripts rewrite it; run /pmap-code:analyze to rebuild the plan (--clean starts over).",
66 ].join(" ");
67}
68
69function mentionsSecret(text) {
70 return (
71 (/\.provenmap/.test(normalise(text)) && SECRET_NAME.test(text)) ||
72 text.split(/[\s;&|<>()`]+/).some((w) => SECRET_DIR.test(unquote(w)))
73 );
74}
75
76// A shell command that could print a credential: a substitution around one, or a
77// segment naming one whose program reads files (or reads it through `<`). Once the
78// command names .provenmap, a bare file name counts too (`cd .provenmap && cat …`).
79function shellReadsSecret(command) {
80 if (!mentionsSecret(command)) return false;
81 if (/`|\$\(/.test(command)) return true;
82 return shellSegments(command).some((segment) => {
83 if (!SECRET_NAME.test(segment) && !mentionsSecret(segment)) return false;
84 const [program, ...args] = segment.trim().split(/\s+/);
85 if (program === "git") return args[0] !== "check-ignore";
86 return !SAFE_PROGRAM.test(program) || segment.includes("<");
87 });
88}
89
90// A shell write into a script-owned file: a redirect target, or an argument of a
91// program that changes the files it names (cp only writes its last one).
92function shellWriteTarget(command) {
93 for (const m of command.matchAll(/>>?\s*["']?([^\s;&|"'<>]+)/g)) {
94 if (SCRIPT_OWNED.test(normalise(m[1]))) return normalise(m[1]);
95 }
96 for (const segment of shellSegments(command)) {
97 const [program, ...args] = segment.trim().split(/\s+/).map(unquote);
98 const inPlace = /^(sed|perl)$/.test(program) && args.some((a) => /^-[a-zA-Z]*i/.test(a));
99 const targets =
100 program === "cp" ? args.slice(-1) : ["rm", "mv", "tee", "truncate"].includes(program) || inPlace ? args : [];
101 const hit = targets.find((w) => SCRIPT_OWNED.test(w));
102 if (hit) return hit;
103 }
104 return null;
105}
106
107/**
108 * The deny text for one tool call, or null to let it through. Exported for the
109 * plugin repo's tests; the engine only calls `register`.
110 */
111export function guard(tool, input) {
112 if (!input || typeof input !== "object") return null;
113 if (tool === "Read") {
114 const p = normalise(input.file_path);
115 return SECRET_FILE.test(p) ? secretDeny(p) : null;
116 }
117 if (tool === "Grep") {
118 const p = normalise(input.path);
119 return SECRET_FILE.test(p) || SECRET_DIR.test(p) ? secretDeny(p) : null;
120 }
121 if (tool === "Edit" || tool === "Write" || tool === "NotebookEdit") {
122 const p = normalise(input.file_path ?? input.notebook_path);
123 if (SECRET_FILE.test(p)) return secretDeny(p);
124 return SCRIPT_OWNED.test(p) ? scriptOwnedDeny(p) : null;
125 }
126 if (tool === "Bash" || tool === "PowerShell") {
127 const command = typeof input.command === "string" ? input.command : "";
128 if (SECRET_ENV.test(command)) return secretDeny(command.match(SECRET_ENV)[0]);
129 if (shellReadsSecret(command)) return secretDeny((command.match(SECRET_NAME) ?? [".provenmap/"])[0]);
130 const written = shellWriteTarget(command);
131 return written ? scriptOwnedDeny(written) : null;
132 }
133 return null;
134}
135
136function redactText(text) {
137 return text.replace(SECRET_VALUE, "$1****");
138}
139
140function redactDeep(value) {
141 if (typeof value === "string") return redactText(value);
142 if (Array.isArray(value)) return value.map(redactDeep);
143 if (value && typeof value === "object") {
144 return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v)]));
145 }
146 return value;
147}
148
149/**
150 * What a tool call resolved to, with every ProvenMap secret masked, or the same
151 * object when it holds none (so core keeps its own rendering). A rewritten result
152 * drops `ref` and `text`, which name the unmasked output, and core re-renders it.
153 */
154export function redact(outcome) {
155 if (!outcome || outcome.deny !== undefined) return outcome;
156 const seen = typeof outcome.text === "string" ? outcome.text : JSON.stringify(outcome.result) ?? "";
157 if (!seen.match(SECRET_VALUE)) return outcome;
158 if (outcome.isError) return { deny: redactText(seen) };
159 return {
160 result: redactDeep(outcome.result),
161 ...(outcome.context ? { context: outcome.context.map(redactText) } : {}),
162 };
163}
164
165/**
166 * When the hook fails, the call goes through (a broken rule must not block every
167 * tool in every session), but its output is still masked, and output that cannot
168 * be masked is masked as text or withheld. `next` here replays a call already made.
169 */
170export async function recover($, e, next) {
171 const outcome = await next(e);
172 try {
173 return redact(outcome);
174 } catch {
175 return typeof outcome?.text === "string"
176 ? { deny: redactText(outcome.text) }
177 : { deny: "ProvenMap guard could not check this output for ProvenMap secrets, so it was withheld." };
178 }
179}
180
181/** @type {import('claude-code').Register} */
182export const register = (on) => {
183 on("tool.call", async ($, e, next) => {
184 const deny = guard(e.tool, e);
185 return deny ? { deny } : redact(await next(e));
186 }).catch(recover);
187};
188