SLOPSHOPPER

ProvenMap Code

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

newguard
★ 3v1.3.6BUSL-1.1updated 2026-10-09provenmap/pmap-claude/plugins/code/claude/pmap-code
A shopper browsing a rack in a slop shop
README

ProvenMap Code

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.

Install

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-code is the install id — everywhere else it's ProvenMap Code.

Configure

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"
}
FieldFileRequiredNotes
bindingTokencredentials.jsonyesBase64url workspaceId::bindingId from the portal. Sent as X-CodePlugin-Token.
apiSecretcredentials.jsonyesStarts 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.
boardSlugconfig.jsonyesTarget board. /configure can discover and write it for you.
branchconfig.jsonyesMust match the binding, or /sync returns 400 Branch Mismatch.
baseUrlconfig.jsonnoOverride for self-hosted (default https://platform.provenmap.com/api).
excludePathsconfig.jsonnoPaths skipped during analysis (default node_modules, dist, .git, coverage).
includeTestsconfig.jsonnoInclude test files in analysis (default false).
includeSourceReferencesconfig.jsonnoAttach file-path references to synced nodes/edges (default true).
analysis.minorFilesconfig.jsonnoHow 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.minorMaxLinesconfig.jsonnoLine 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-url to 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 /login writes 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_URL wins.

Quick start

  1. /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.
  2. /login (browser) or /configure (manual) — connect to the portal (one-time per repo)
  3. /analyze — full architecture analysis (incremental — only changed files re-analyzed)
  4. /sync — push the analysis to your board
  5. /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)
  6. /insights — run server-defined analyses (security, performance, etc.) against the board
  7. /status — see what's analyzed, synced and grounded

/analyze-archetypes is optional — see Archetypes.

Supported languages

Automatically detects projects in 7+ languages via their manifest files:

LanguageManifestCommon frameworks
JS/TypeScriptpackage.jsonNext.js, NestJS, Express, React, Angular, Vue
Pythonrequirements.txt, pyproject.toml, PipfileDjango, FastAPI, Flask, Celery
Javapom.xml, build.gradleSpring Boot, Quarkus, Micronaut
Gogo.modGin, Echo, Fiber, gRPC
C# / .NET*.csproj, *.slnASP.NET Core, Blazor, EF
RubyGemfileRails, Sinatra, Sidekiq
RustCargo.tomlActix, Axum, Rocket

Polyglot projects produce a unified board with cross-language relationships (HTTP, gRPC, message queues).

Commands

CommandDescription
/startStart here — the surfaces card, the ranked next step, and a menu to run it
/loginBrowser sign-in: pick a workspace + bound board, writes config automatically
/configureSet up portal credentials manually, or switch to a different board
/analyze [path]Analyze codebase architecture (incremental; re-analyzes only changed files)
/analyze --cleanFull re-analysis from scratch, ignoring existing board data
/analyze --drill <board>/<node>Drill into a node to produce a child layer board
/analyze --allRe-analyze every layer board in the manifest
/analyze --scheduledUnattended 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 --allPush 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
/insightsList available insight skills and run one against the current board
/insights <skill-slug>Run a specific insight skill directly
/insights --allRun 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-itemsPull 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 setupCorrelate monitoring signals (errors, logs, cloud costs) with the board and push findings as a draft insight; setup configures sources + a recurring run
/statusShow the lifecycle dial, config state, analysis summary, and per-board sync status
/helpList commands grouped by lifecycle stage, with the plugin version
/updateUpdate this plugin to the latest published version for your host
/analyze-archetypesAdvanced — customize the archetype vocabulary: scan for gaps, submit proposals for admin review
/analyze-archetypes --dry-runValidate scan locally + ask server to dry-run, don't persist or POST
/analyze-archetypes --skip-submitWrite the proposals file for manual review; don't POST
/analyze-archetypes --replaceWhen submitting, send mode='replace' to overwrite pending payload

Grounding a board in documents

/ground keeps a board honest against the documents that back it. It has two shapes:

  • A code repo /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.
  • A documents-only repo. The architect authors the board in ProvenMap; /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.

Layered boards

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:

LayerNameScopeTarget node count
L0System ContextThis system's deployables + the outside systems they talk to10–30
L1DomainDomain or workspace drill-down10–40 per board
L2ComponentService or module drill-down5–20 per board
L3+DetailDeep 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 board
  • stores/<board-slug>.store.json — sync state, content hashes, last push

Archetypes (server-defined, settlement optional)

Components 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.

Advanced: making settlement a precondition

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.

Output format

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.

Agents

AgentRole
architecture-analyzerMulti-language structure analysis, archetype classification, domain grouping, hierarchy building
relationship-detectorImports, 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.

Requirements

  • Node.js (for the bundled CLI scripts)
  • A ProvenMap Portal account with a Board Builder source bound to your board
  • Git repo (incremental analysis uses git diff for change detection)

License

BUSL-1.1

Source 1 files
hooks/pmap-guard.js 188 lines
1// 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