Skip to content

Instantly share code, notes, and snippets.

@Kinark
Last active July 17, 2026 09:58
Show Gist options
  • Select an option

  • Save Kinark/55c0de759b451b6eb517a2692dca2dac to your computer and use it in GitHub Desktop.

Select an option

Save Kinark/55c0de759b451b6eb517a2692dca2dac to your computer and use it in GitHub Desktop.
docmap β€” a fully sub-agent-driven Claude Code skill touple (docmap + undocmap) that builds/updates a hierarchical CLAUDE.md + path-scoped .claude/rules guidance map for any codebase. Comes with (un)installer(s).sh πŸ˜€

docmap + undocmap β€” Claude Code skills

Two companion skills for maintaining an LLM/agent guidance map in any codebase:

  • /docmap β€” build or update the map with a single command: a lean root CLAUDE.md, a detailed CLAUDE.md for every top-level unit, and long single-concern sections migrated into path-scoped .claude/rules/ files.
  • /undocmap β€” tear it all back down.

Both are fully sub-agent driven via a Claude Code Workflow: the model driving your main loop never enumerates folders or writes docs itself. Every step runs on an explicitly pinned model β€”

  • Sonnet 5 β€” enumeration / git-delta + per-folder authoring (and undocmap's inventory + removal)
  • Opus 4.8 β€” the migration judgment (which sections become path-scoped rules) + root-map synthesis, conflict resolution, and the manifest

⚠️ Both are override tools β€” read this

  • /docmap OWNS every CLAUDE.md it writes. If one already exists (root or per-folder) it's read only as a dispensable hint β€” salvaging still-accurate facts β€” then overwritten. It does not preserve arbitrary hand-written prose.
  • /undocmap DELETES every CLAUDE.md docmap owns, its .claude/rules/ files, and the manifest β€” with no confirmation prompt. Deletion is irreversible.
  • Keep anything you can't lose in CLAUDE.local.md (neither tool touches it) or commit to git first. Every file docmap writes ends with an uppercased note telling future agents to keep it in sync when they change what it describes.

What it produces

your-repo/
β”œβ”€β”€ CLAUDE.md                     # lean, always-on index (loads in full every session)
β”œβ”€β”€ .claude/
β”‚   β”œβ”€β”€ docmap.json               # manifest (NOT loaded into context β€” makes updates cheap)
β”‚   └── rules/
β”‚       └── <unit>/
β”‚           └── <concern>.md      # path-scoped: loads only when matching files are touched
β”œβ”€β”€ src/
β”‚   └── CLAUDE.md                 # per-folder detail (loads on demand)
└── ...

This layout mirrors how Claude Code actually loads memory: root CLAUDE.md loads in full every session (so it stays a lean index), subfolder CLAUDE.md loads on demand, and .claude/rules/*.md with a paths: glob load only when matching files are touched. Detail is pushed down and lazy.

Auto-detect: build vs update

One command, /docmap, decides what to do:

  • No .claude/docmap.json β†’ build: enumerate units β†’ author a CLAUDE.md each β†’ migrate long sections into path-scoped rules β†’ write the root map + manifest.
  • Manifest present β†’ update: diff-driven. Uses git diff --name-only since the last sync commit and only re-touches units whose files changed β€” untouched units are never opened (the token win). Says "up to date" and stops if nothing changed.
  • Force a rebuild: pass { force: true } (see below).

Install

# user-level (available in every project): default
. <(curl docmap.dotz.sh)

# or install into the current repo only:
. <(curl docmap.dotz.sh) --project

docmap.dotz.sh is a short alias for this gist's install.sh. The installer is written so it's safe to source it (. <(...)) β€” all the real work runs in a nested bash, so it never changes your shell options, leaks variables, or exits your shell, and it ignores the process-substitution path your shell passes in. It installs both skills into ~/.claude/skills/docmap/ and ~/.claude/skills/undocmap/ (or under ./.claude/skills/ with --project), each with a .docmap-version stamp recording the version, install time, and source. Re-running it upgrades in place.

Piping to bash works too, and is equivalent:

curl docmap.dotz.sh | bash                 # user-level
curl docmap.dotz.sh | bash -s -- --project # repo-only

Prefer to read before running? curl docmap.dotz.sh and eyeball it first β€” it's short.

Uninstall

# remove the user-level install
. <(curl docmap.dotz.sh) -u

# remove a project-level install
. <(curl docmap.dotz.sh) --project -u

-u (--uninstall) removes both skill directories for the chosen scope. It refuses to delete a directory that doesn't look like one of these installs, and it does not touch any CLAUDE.md, .claude/rules/, or .claude/docmap.json that /docmap generated in your repos β€” use /undocmap or delete those per-repo if you want them gone.

Flags

Flag Meaning
(none) install both skills, user-level (~/.claude/skills/) β€” available in every project
--project, -p target the current repo (./.claude/skills/)
--user target user-level β€” this is already the default, so the flag just documents intent
--uninstall, --remove, --delete, -u, -r, -d remove both skills for the chosen scope
-h, --help show help

Usage

In any repo, in Claude Code:

/docmap      # build the map (first run) or sync it (later runs)
/undocmap    # delete the whole map β€” destructive, no confirmation
  • Run /docmap again anytime to sync after code changes.
  • Force a full rebuild over an existing map: tell Claude to run the skill with args: { force: true } (or invoke the workflow directly with { force: true }).
  • /undocmap warns you before deleting, then removes every CLAUDE.md docmap owns, its rules, and the manifest. It leaves .claude/ itself, your settings, and CLAUDE.local.md alone.

Requirements

  • Claude Code with the Workflow tool available (the skill includes an Agent-tool fallback if Workflow isn't present).
  • git (the map is derived from tracked files, so .gitignore is respected automatically).

Notes

  • Monorepos: if the top level is essentially just packages/ apps/ services/ libs/, it descends one level and treats each package/app as a unit.
  • Small repos: it bails out with a note rather than over-engineering a tiny project.
  • It never invents behavior β€” workers write less rather than speculate β€” and respects .gitignore, never documenting generated/vendored dirs.

License

MIT β€” do whatever you want.

// Stub file to customize the Gist title
Error in user YAML: (<unknown>): mapping values are not allowed in this context at line 2 column 252
---
name: docmap
description: Build or update a hierarchical LLM/agent guidance map for the current codebase β€” a lean root CLAUDE.md, a detailed CLAUDE.md per top-level unit, and long single-concern sections migrated into path-scoped .claude/rules/ files. Auto-detects: no .claude/docmap.json manifest β†’ full build; manifest present β†’ cheap diff-driven update (only re-touches changed units). Fully sub-agent driven via a Workflow β€” enumeration/delta + per-folder authoring run on Sonnet 5, migration + root-map/manifest synthesis on Opus 4.8; the dispatching model does no authoring. Use when the user asks to "document the codebase for Claude", "generate/update CLAUDE.md files", "map a large repo for agents", "bootstrap or refresh agent guidance", "set up .claude/rules", or "sync the docmap".
---

docmap

This skill is a dispatcher. All real work runs inside a deterministic Workflow whose every step is a sub-agent on an explicitly pinned model β€” so the model driving the main loop never enumerates folders or writes docs itself. One command handles both first-time build and ongoing upkeep: it auto-detects which is needed.

⚠️ Override tool β€” warn the user

docmap owns every CLAUDE.md it writes. If a CLAUDE.md already exists (root or per-folder), docmap reads it only as a dispensable hint β€” salvaging still-accurate facts β€” then overwrites it. It does not preserve arbitrary hand-written prose. Mention this when you launch the skill so the user isn't surprised; anything they can't lose should live in CLAUDE.local.md (which docmap never touches) or be committed to git first. Every file docmap writes ends with an uppercased self-maintenance note telling future agents to keep it in sync.

What to do

Call the Workflow tool with the script that ships next to this skill, docmap.workflow.js. Use its absolute path β€” expand ~ to the current user's home directory (the skill lives at <home>/.claude/skills/docmap/docmap.workflow.js for a user-level install, or <repo>/.claude/skills/docmap/docmap.workflow.js for a project install):

Workflow({ scriptPath: "<home>/.claude/skills/docmap/docmap.workflow.js" })

The user invoking this skill is the explicit opt-in for the Workflow tool. Do not do the work inline. Launch the workflow, then relay its returned summary to the user (mode, units authored / added / removed, rules created / retired / re-scoped, whether the root map changed).

  • Force a full rebuild over an existing docmap: Workflow({ scriptPath: "...", args: { force: true } }).
  • The workflow runs in the background; read its return value on completion and summarize β€” do not re-derive the result yourself.

Auto-detect behavior (build vs update)

  • No .claude/docmap.json β†’ build mode: enumerate units, author a CLAUDE.md for each, migrate long sections, write the root map + manifest.
  • Manifest present β†’ update mode: diff-driven. Uses git diff --name-only since the last sync commit; only re-touches units whose files changed; leaves untouched units unopened (the token win). Reports "up to date" and stops if nothing changed.
  • force: true β†’ treat as build even if a manifest exists.

What the workflow does (for your awareness β€” do not re-implement inline)

  1. Assess (Sonnet 5) β€” detects mode. Build: enumerates top-level units from tracked files (git-ignore aware), descends into monorepo packages/apps/services/libs, applies a small-repo guard. Update: computes the git delta β†’ changed / new / deleted units. Cheap (file names only).
  2. Author (Sonnet 5, one worker per unit) β€” scoped to a single folder. New unit β†’ writes a fresh agent-focused <unit>/CLAUDE.md; changed unit β†’ reads only changed files and edits in place, diffing meaning not text so stable sections stay byte-for-byte.
  3. Migrate (Opus 4.8, pipelined per unit) β€” the moment a unit's CLAUDE.md is written, Opus migrates any touched section that is >20–25 lines AND covers one concern into .claude/rules/<unit>/<concern>.md, path-scoped with paths: glob frontmatter when it only applies to certain file types/subpaths; leaves a one-line pointer. Also retires orphaned rules and fixes globs that no longer match.
  4. Finalize (Opus 4.8) β€” deletes docs/rules for removed units, writes/refreshes the lean always-on root CLAUDE.md index (only if structure/commands changed, in update mode), resolves cross-unit conflicts, and rewrites .claude/docmap.json so the next run stays cheap.

Why this shape (grounded in Claude Code memory loading)

Root CLAUDE.md loads in full every session (keep it a lean index); subfolder CLAUDE.md loads on demand; .claude/rules/*.md with a paths: glob load only when matching files are touched (rules without paths: load at launch). Detail is pushed down and lazy; only genuinely global rules stay always-on. The .claude/docmap.json manifest is never loaded into context β€” zero token cost.

If the Workflow tool is unavailable

Fall back to the Agent tool with the same tiers and contract: an Assess scout (Sonnet 5) detects mode + scope, an Opus 4.8 orchestrator fans out one Sonnet 5 worker per unit, then runs the migration + finalize passes. The model tiers and per-step contract above are the requirement; the workflow just guarantees them deterministically.

export const meta = {
name: 'docmap',
description: 'Build or update a hierarchical LLM/agent guidance map for the current codebase. OVERWRITE TOOL: docmap owns every CLAUDE.md it writes β€” existing CLAUDE.md files are read as a dispensable hint and then overwritten. Auto-detects: no .claude/docmap.json manifest -> full build; manifest present -> cheap diff-driven update. Fully sub-agent driven; enumeration/delta + per-unit authoring run on Sonnet 5, migration + root-map/manifest synthesis on Opus 4.8. The dispatching model does no authoring.',
phases: [
{ title: 'Assess', detail: 'Detect mode; enumerate units (build) or compute git delta (update). Small-repo guard.', model: 'sonnet' },
{ title: 'Author', detail: 'One Sonnet 5 worker per unit writes/updates <unit>/CLAUDE.md', model: 'sonnet' },
{ title: 'Migrate', detail: 'Opus 4.8 migrates >20-25 line single-concern sections into path-scoped rules', model: 'opus' },
{ title: 'Finalize', detail: 'Opus 4.8 writes/refreshes root map, resolves conflicts, writes manifest', model: 'opus' },
],
}
// ---- shared spec text (identical guidance across workers) --------------------
const AUDIENCE = `
AUDIENCE: the ONLY readers are LLMs and coding agents navigating this repo. Not humans
onboarding, not marketing. Optimize for "where things live, how they connect, and the
conventions/pitfalls an agent CANNOT infer by reading the code."
HOW CLAUDE CODE LOADS THIS (why the structure is shaped this way):
- Root CLAUDE.md loads IN FULL every session -> it must stay a lean always-on index.
- A subfolder CLAUDE.md loads ON DEMAND, only when an agent reads a file in that folder.
- .claude/rules/*.md with a "paths:" glob load ON DEMAND, only when matching files are touched.
- A rule with NO "paths:" loads at launch (as heavy as root CLAUDE.md) -> avoid unless truly global.
- HTML comments (<!-- ... -->) in CLAUDE.md are STRIPPED from context: maintainer notes at zero cost.
WRITING RULES:
- Factual and specific. NEVER speculate; if unsure, write less.
- Concrete + verifiable ("2-space indent", "handlers live in ./handlers/") not vague ("format nicely").
- Do NOT restate what an agent can read from the code. Favor pitfalls, rationale, non-default conventions.
- Keep the file under ~200 lines.`
const UNIT_SECTIONS = `
Sections (omit any that genuinely don't apply):
- Purpose: what this folder is responsible for (1-3 lines).
- Layout: key files/subdirs and what each does (the local map).
- Fits into: entry points, who calls in, what it depends on out.
- Conventions: naming/patterns/idioms SPECIFIC to this folder that differ from repo/tool defaults.
- Common tasks: "to add/change an X, edit Y" pointers.
- Pitfalls: non-obvious constraints, footguns, ordering requirements.
- Commands: build/test/run relevant to THIS folder, if any.`
const slug = (s) => s.replace(/[^a-zA-Z0-9._-]/g, '-')
// Self-maintenance footer appended VERBATIM as the last line of every file docmap writes.
// Plain text (NOT an HTML comment) so agents actually see it β€” HTML comments are stripped from context.
const FOOTER =
'> NOTE FOR ANY AGENT: THIS FILE IS MAINTAINED BY THE `docmap` SKILL. IF YOU CHANGE CODE, ' +
'BEHAVIOR, OR CONVENTIONS THAT THIS FILE (OR A RULE IT POINTS TO) DESCRIBES, UPDATE THE ' +
'AFFECTED FILE/RULE IN THE SAME CHANGE SO THE GUIDANCE STAYS TRUE β€” OR RE-RUN `/docmap` TO RESYNC.'
const FOOTER_RULE =
`FOOTER: every CLAUDE.md and rule file you write MUST end with exactly this line as its final line ` +
`(verbatim, unchanged):\n${FOOTER}\nWhen editing a file that already has this footer, keep it last and do not duplicate it.`
// ---- schemas -----------------------------------------------------------------
const ASSESS_SCHEMA = {
type: 'object', additionalProperties: false,
required: ['mode', 'qualifies', 'reason', 'repoRoot', 'lastSyncCommit', 'nothingChanged',
'rootRefreshNeeded', 'globalCommands', 'existingRootClaude', 'workItems', 'deletedUnits'],
properties: {
mode: { enum: ['build', 'update'] },
qualifies: { type: 'boolean', description: 'build mode: false for tiny repos where fan-out is overkill. update mode: always true.' },
reason: { type: 'string' },
repoRoot: { type: 'string' },
lastSyncCommit: { type: 'string', description: 'update mode: from manifest; "" in build mode' },
nothingChanged: { type: 'boolean', description: 'update mode only: true if no unit changed and no root refresh' },
rootRefreshNeeded: { type: 'boolean', description: 'build: always true. update: true if units added/removed or root build commands changed.' },
globalCommands: { type: 'string', description: 'primary build/test/run commands for the repo root' },
existingRootClaude: { type: 'boolean' },
workItems: {
type: 'array',
description: 'units to author. build: every unit with isNew=true. update: changed units (isNew=false) + new units (isNew=true).',
items: {
type: 'object', additionalProperties: false, required: ['name', 'path', 'isNew', 'changedFiles', 'ownedRules'],
properties: {
name: { type: 'string' },
path: { type: 'string', description: 'repo-relative folder path' },
isNew: { type: 'boolean', description: 'true = author from scratch; false = update in place from changedFiles' },
changedFiles: { type: 'array', items: { type: 'string' }, description: 'update mode: repo-relative changed paths in this unit; [] in build mode' },
ownedRules: { type: 'array', items: { type: 'string' }, description: 'existing rule files the manifest attributes to this unit; [] in build mode' },
},
},
},
deletedUnits: {
type: 'array',
description: 'update mode only: units whose folder no longer exists on disk',
items: {
type: 'object', additionalProperties: false, required: ['name', 'path', 'ownedRules'],
properties: { name: { type: 'string' }, path: { type: 'string' }, ownedRules: { type: 'array', items: { type: 'string' } } },
},
},
},
}
const AUTHOR_SCHEMA = {
type: 'object', additionalProperties: false,
required: ['unit', 'path', 'action', 'sections'],
properties: {
unit: { type: 'string' },
path: { type: 'string', description: 'path to the CLAUDE.md written/edited' },
action: { enum: ['created', 'updated', 'unchanged'] },
sections: {
type: 'array',
items: {
type: 'object', additionalProperties: false, required: ['title', 'approxLines', 'scope', 'touched'],
properties: {
title: { type: 'string' },
approxLines: { type: 'integer' },
scope: { enum: ['folder', 'filetype', 'subpath'], description: 'folder = whole unit; filetype/subpath = candidate for path-scoped rule' },
candidateGlobs: { type: 'array', items: { type: 'string' } },
touched: { type: 'boolean', description: 'true if written/changed this run (build: all true; update: only edited sections). Migration re-checks only touched sections.' },
},
},
},
},
}
const MIGRATE_SCHEMA = {
type: 'object', additionalProperties: false,
required: ['unit', 'rulesCreated', 'rulesRetired', 'rulesReScoped'],
properties: {
unit: { type: 'string' },
rulesCreated: {
type: 'array',
items: {
type: 'object', additionalProperties: false, required: ['file', 'source', 'paths'],
properties: {
file: { type: 'string', description: '.claude/rules/... path written' },
source: { type: 'string', description: '<unit>/CLAUDE.md#section it came from' },
paths: { type: 'array', items: { type: 'string' }, description: 'globs; [] = always-on rule (no paths: frontmatter)' },
},
},
},
rulesRetired: { type: 'array', items: { type: 'string' }, description: 'rule files deleted (folded back inline or orphaned)' },
rulesReScoped: {
type: 'array',
items: {
type: 'object', additionalProperties: false, required: ['file', 'paths'],
properties: { file: { type: 'string' }, paths: { type: 'array', items: { type: 'string' } } },
},
},
},
}
const FINAL_SCHEMA = {
type: 'object', additionalProperties: false,
required: ['rootClaudePath', 'manifestPath', 'rootChanged', 'conflictsResolved', 'summary'],
properties: {
rootClaudePath: { type: 'string' },
manifestPath: { type: 'string' },
rootChanged: { type: 'boolean' },
conflictsResolved: { type: 'array', items: { type: 'string' } },
summary: { type: 'string', description: 'one-line human summary of what changed' },
},
}
// ---- prompts -----------------------------------------------------------------
const assessPrompt = (force) => `You are the ASSESS scout for a codebase guidance-map run. Work read-only; do NOT write files.
Be cheap: fetch file NAMES, never diff bodies.
STEP A - detect MODE:
- Read ".claude/docmap.json" if it exists.
- If it is MISSING${force ? ', OR force is set (it is)' : ''} -> mode="build".
- Else -> mode="update" (use its lastSyncCommit, units[], rules[]).
repoRoot = \`git rev-parse --show-toplevel\` (if not a git repo, use cwd and note it).
IF mode="build":
1. Enumerate TOP-LEVEL units from TRACKED files only so ignored dirs are auto-skipped:
\`git ls-files | sed 's#/.*##' | sort -u\` (fallback to a plain listing if not git).
Exclude .git and generated/vendored dirs: node_modules, dist, build, out, target, .venv,
venv, vendor, coverage, .next, .cache, and similar.
2. MONOREPO: if the top level is essentially just packages/ apps/ services/ libs/, descend ONE
level and treat each package/app as a unit (path like "packages/api").
3. SMALL-REPO GUARD: if there are only a handful of folders / a few thousand lines total, set
qualifies=false + reason (a single hand-written root CLAUDE.md beats fan-out). Otherwise qualifies=true.
4. workItems = every unit with isNew=true, changedFiles=[], ownedRules=[].
deletedUnits=[]. nothingChanged=false. rootRefreshNeeded=true. lastSyncCommit="".
Detect an existing root CLAUDE.md (./CLAUDE.md or ./.claude/CLAUDE.md) -> existingRootClaude.
IF mode="update":
1. qualifies=true. Compute changed files (NAMES ONLY):
\`git diff --name-only <lastSyncCommit> HEAD\` (skip the range if lastSyncCommit empty/invalid)
plus \`git status --porcelain\` for uncommitted/staged work. If lastSyncCommit is missing/invalid,
do NOT silently full-rebuild: set qualifies=false + reason recommending a forced rebuild.
2. Map changed files to units via units[]:
- workItems: existing units with >=1 changed file (isNew=false, changedFiles filled, ownedRules
from rules[] belonging to that unit) PLUS brand-new top-level folders/packages not in units[]
(isNew=true, changedFiles=[], ownedRules=[]), applying the same ignore/generated-dir exclusions.
- deletedUnits: units[] whose folder no longer exists on disk (+ ownedRules).
3. rootRefreshNeeded = units added/removed OR root build files changed (package.json, lockfiles,
Makefile, pyproject, cargo.toml, CI config). globalCommands = current root build/test/run commands.
4. nothingChanged = true ONLY if workItems, deletedUnits are empty AND rootRefreshNeeded is false.
Return the structured result. All paths repo-relative.`
const authorPrompt = (item) => item.isNew
? `You are a scoped AUTHOR worker for unit "${item.name}" at repo-relative path "${item.path}" (fresh doc).
Read ONLY files under "${item.path}" (glance at root entry points/config for inbound/outbound edges).
If the unit is large, sample by subdir; do not read everything.
Write "${item.path}/CLAUDE.md" (Write tool β€” the file IS your deliverable). docmap OWNS this file:
if a CLAUDE.md already exists here, read it FIRST as an optional, DISPENSABLE hint (salvage any
still-accurate facts worth keeping), then OVERWRITE it with your clean version. Do not preserve stale
or off-topic human prose β€” docmap is the source of truth for this file.
${AUDIENCE}
${UNIT_SECTIONS}
${FOOTER_RULE}
Longer single-concern sections are OK; a later Opus pass migrates any that qualify. Return the
structured summary: sections with APPROX line counts, a scope flag (folder = whole unit;
filetype/subpath = only certain file types/subpath, with candidateGlobs like ["${item.path}/**/*.ts"]),
and touched=true for ALL sections (this is a fresh file). action="created". Return data, not prose.`
: `You are a scoped UPDATE worker for unit "${item.name}" at "${item.path}".
Files changed in this unit since the last sync:
${JSON.stringify(item.changedFiles, null, 2)}
Read ONLY those changed files, plus the current "${item.path}/CLAUDE.md" and this unit's owned rule
files: ${JSON.stringify(item.ownedRules || [], null, 2)}.
${AUDIENCE}
DIFF MEANING, NOT TEXT. Update ONLY sections whose facts actually changed: layout entries for
added/renamed/deleted files, conventions that shifted, commands that changed, pitfalls no longer
true. Leave correct sections BYTE-FOR-BYTE untouched (stable = no token spend, no churn). Use the
Edit tool for in-place edits; do not rewrite the whole file. Sections: ${UNIT_SECTIONS.trim()}
${FOOTER_RULE}
Return the structured summary. Mark touched=true ONLY on sections you actually changed (migration
re-checks only those). action="updated" if you changed anything, else "unchanged".`
const migratePrompt = (author, item) => `You are the MIGRATION worker (Opus-tier judgment) for unit "${item.name}" at "${item.path}".
The author worker's section summary:
${JSON.stringify(author.sections, null, 2)}
Read "${item.path}/CLAUDE.md" and apply the migration rule to TOUCHED sections only (skip untouched
sections β€” already settled). Migrate a section OUT into its own rule file when BOTH hold:
(a) it exceeds ~20-25 lines, AND
(b) it covers ONE specific, cohesive concern (testing, api-error-format, db-migrations, styling,
auth, ...), self-contained β€” not a grab-bag.
If a long section is actually several concerns, SPLIT it first, then migrate the qualifying pieces.
For each migrated section:
- Write it to ".claude/rules/${slug(item.name)}/<concern>.md" (kebab-case concern, one per file;
nesting under the unit avoids clashes β€” rules are discovered recursively).
- PATH-SCOPE whenever it only applies to certain file types/subpath. Narrowest correct glob(s) in
YAML frontmatter:
---
paths:
- "${item.path}/**/*.ts"
---
Brace expansion ("**/*.{ts,tsx}") and multiple patterns allowed. Prefer path-scoped (loads only
when matching files are touched). Omit "paths:" ONLY for concerns relevant folder-wide+ every session.
- In "${item.path}/CLAUDE.md", REPLACE the migrated section with a one-line navigation pointer, e.g.
"- Testing: see .claude/rules/${item.name}/testing.md". (Rules load on their own; pointer is for browsing.)
- Verify each glob matches >=1 real file (a glob matching nothing silently does nothing).
ALSO reconcile existing rules for this unit:
- A touched section that SHRANK back under threshold or lost single-concern focus -> optionally fold
back inline and DELETE the orphaned rule (report in rulesRetired).
- Any owned rule whose "paths:" glob no longer matches a real file (files moved/renamed) -> fix it
(rulesReScoped) or delete it (rulesRetired).
Use Edit for CLAUDE.md, Write for new rules, and actually delete retired rule files.
${FOOTER_RULE}
Return the result (rulesCreated paths=[] means a deliberate always-on rule).`
const finalizePrompt = (assess, migrations, authored) => `You are the FINALIZER (Opus-tier). Mode="${assess.mode}". All units authored + migrated. Data:
ASSESS: ${JSON.stringify({ repoRoot: assess.repoRoot, globalCommands: assess.globalCommands, existingRootClaude: assess.existingRootClaude, rootRefreshNeeded: assess.rootRefreshNeeded, workItems: assess.workItems.map(w => ({ name: w.name, path: w.path, isNew: w.isNew })), deletedUnits: assess.deletedUnits }, null, 2)}
AUTHORED: ${JSON.stringify(authored, null, 2)}
MIGRATIONS: ${JSON.stringify(migrations, null, 2)}
1. DELETED UNITS (if any): delete each one's CLAUDE.md and owned rule files if not already gone.
2. ROOT CLAUDE.md: docmap OWNS this file and (re)writes it in full. ${assess.mode === 'build'
? 'Write it (prefer ./CLAUDE.md; use ./.claude/CLAUDE.md if that already exists).'
: 'Rewrite it if rootRefreshNeeded is true; otherwise leave it as docmap last wrote it and set rootChanged=false.'}
It loads IN FULL every session -> keep it a LEAN always-on INDEX, well under 200 lines:
- one-line repo purpose + primary build/test/run commands,
- a "Where things live" list: one line per unit -> its folder + its CLAUDE.md,
- only genuinely GLOBAL "always do X" conventions.
OVERWRITE SEMANTICS: if a root CLAUDE.md already exists, you may READ it as an optional, DISPENSABLE
hint (salvage any still-accurate build commands or conventions worth keeping), then REPLACE the file
with docmap's clean index. Do NOT try to preserve arbitrary human content β€” docmap is the source of
truth for this file. For an update, ensure the "Where things live" list reflects added/removed units.
${FOOTER_RULE}
3. CONFLICT CHECK: scan unit CLAUDE.md files + .claude/rules/* for directly contradicting instructions
(Claude picks arbitrarily on conflict). Resolve each; list them. Verify no navigation pointer
references a missing rule file.
4. MANIFEST ".claude/docmap.json" (NOT loaded into context β€” zero token cost):
{
"schema": 2,
"lastSyncCommit": "<git rev-parse HEAD>", // "" if not a git repo
"root": { "file": "<repo-relative path to the root CLAUDE.md you wrote>" },
"units": [ ...current repo-relative unit paths (existing minus deleted plus new)... ],
"rules": [ { "file": "...", "source": "...", "paths": [...] }, ... ]
}
${assess.mode === 'update' ? 'Start from the prior rules[] and root object, then add rulesCreated, drop rulesRetired, apply rulesReScoped globs.' : 'Build rules[] from MIGRATIONS.rulesCreated.'}
Run \`git rev-parse HEAD\` yourself for lastSyncCommit.
Return the structured result.`
// ---- orchestration -----------------------------------------------------------
const opts = (typeof args === 'object' && args) ? args : {}
const force = !!opts.force
phase('Assess')
const assess = await agent(assessPrompt(force), {
label: 'assess', phase: 'Assess', model: 'sonnet', agentType: 'general-purpose', schema: ASSESS_SCHEMA,
})
if (!assess) return { skipped: true, reason: 'assess stage failed' }
if (!assess.qualifies) {
log(`Skipping: ${assess.reason}`)
return { skipped: true, mode: assess.mode, reason: assess.reason }
}
if (assess.mode === 'update' && assess.nothingChanged) {
log('docmap already up to date β€” nothing changed since last sync.')
return { upToDate: true, lastSyncCommit: assess.lastSyncCommit }
}
log('⚠️ docmap OWNS every CLAUDE.md it writes: existing CLAUDE.md files are treated as a dispensable ' +
'hint and OVERWRITTEN. Salvageable facts are kept, arbitrary human prose is not.')
const workItems = assess.workItems || []
log(`${assess.mode === 'build' ? 'Building' : 'Updating'} docmap: ${workItems.length} unit(s)` +
(assess.deletedUnits && assess.deletedUnits.length ? `, ${assess.deletedUnits.length} deleted` : '') +
(workItems.length ? ` β€” ${workItems.map(w => w.name).join(', ')}` : ''))
// author (Sonnet 5) -> migrate (Opus 4.8), pipelined per unit: Opus migrates unit A the moment its
// CLAUDE.md is written while Sonnet still authors unit B. Per-unit work is independent.
const authored = []
const migrations = (await pipeline(
workItems,
(item) => agent(authorPrompt(item), {
label: `author:${item.name}`, phase: 'Author', model: 'sonnet', agentType: 'general-purpose', schema: AUTHOR_SCHEMA,
}),
(author, item) => {
if (!author) return null
authored.push({ unit: author.unit, action: author.action })
if (author.action === 'unchanged') return null // nothing to migrate
return agent(migratePrompt(author, item), {
label: `migrate:${item.name}`, phase: 'Migrate', model: 'opus', agentType: 'general-purpose', schema: MIGRATE_SCHEMA,
})
},
)).filter(Boolean)
// Barrier: deletions, root map + cross-unit conflict resolution, and the manifest need every result.
phase('Finalize')
const final = await agent(finalizePrompt(assess, migrations, authored), {
label: 'finalize', phase: 'Finalize', model: 'opus', agentType: 'general-purpose', schema: FINAL_SCHEMA,
})
return {
mode: assess.mode,
units: workItems.map(w => w.name),
newUnits: workItems.filter(w => w.isNew).map(w => w.name),
deletedUnits: (assess.deletedUnits || []).map(w => w.name),
migratedUnits: migrations.length,
...final,
}
#!/usr/bin/env bash
#
# Installer for the "docmap" + "undocmap" Claude Code skills.
#
# Runs safely whether you SOURCE it or EXECUTE it β€” all real work happens inside a nested
# `bash -s`, so sourcing never changes shell options or exits your shell:
#
# . <(curl docmap.dotz.sh) # source (recommended)
# . <(curl docmap.dotz.sh) --project # source, repo-local
# . <(curl docmap.dotz.sh) -u # source, uninstall
# curl docmap.dotz.sh | bash # pipe
# curl docmap.dotz.sh | bash -s -- -u # pipe, uninstall
# bash install.sh [--project] [-u] # execute a local copy
#
# Args after the URL are forwarded to the installer (see --help).
# When sourced via `. <(curl ...)`, the sourcing shell can pass the process-substitution
# path (e.g. /dev/fd/11) as a positional parameter. Every real option starts with "-", so
# forward only those and drop any stray path. Array form works in both bash and zsh.
_docmap_args=()
for _a in "$@"; do
case "$_a" in
-*) _docmap_args+=("$_a") ;;
esac
done
bash -s -- "${_docmap_args[@]}" <<'DOCMAP_INSTALLER_EOF'
set -euo pipefail
VERSION="2.2.0"
GIST_ID="55c0de759b451b6eb517a2692dca2dac"
GIST_RAW="https://gist.githubusercontent.com/Kinark/${GIST_ID}/raw"
GIST_GIT="https://gist.github.com/${GIST_ID}.git"
STAMP=".docmap-version"
# Skills to install. Gists are flat, so SKILL.md files are namespaced in the gist
# (docmap.SKILL.md) and un-namespaced on disk (docmap/SKILL.md).
SKILLS=("docmap" "undocmap")
declare_files() {
case "$1" in
docmap) echo "docmap.SKILL.md:SKILL.md docmap.workflow.js:docmap.workflow.js" ;;
undocmap) echo "undocmap.SKILL.md:SKILL.md undocmap.workflow.js:undocmap.workflow.js" ;;
esac
}
# --- parse args ---------------------------------------------------------------
SCOPE="user"
ACTION="install"
for arg in "$@"; do
case "$arg" in
--project|-p) SCOPE="project" ;;
--user) SCOPE="user" ;; # explicit alias for the default; documents intent
--uninstall|--remove|--delete|-u|-r|-d) ACTION="uninstall" ;;
-h|--help)
cat <<'USAGE'
Usage: install.sh [options] (or: . <(curl docmap.dotz.sh) [options])
(no options) install to ~/.claude/skills/{docmap,undocmap} (user-level, every project)
--project, -p target ./.claude/skills/... (this repo only)
--user target ~/.claude/skills/... (default; explicit alias)
--uninstall, --remove, remove both skills from the chosen scope
--delete, -u, -r, -d
-h, --help show this help
USAGE
exit 0 ;;
*) echo "Unknown option: $arg" >&2; exit 2 ;;
esac
done
if [ "$SCOPE" = "project" ]; then
SKILLS_ROOT="$(pwd)/.claude/skills"
else
SKILLS_ROOT="${HOME}/.claude/skills"
fi
# --- uninstall ----------------------------------------------------------------
if [ "$ACTION" = "uninstall" ]; then
echo "==> Uninstalling docmap + undocmap skills from: ${SKILLS_ROOT}"
any=0
for skill in "${SKILLS[@]}"; do
dest="${SKILLS_ROOT}/${skill}"
if [ ! -d "$dest" ]; then
echo " - ${skill}: not installed, skipping"
continue
fi
# Safety: only remove a dir that actually looks like our install.
if [ ! -f "$dest/${skill}.workflow.js" ] || [ ! -f "$dest/SKILL.md" ]; then
echo " ! ${skill}: ${dest} does not look like a ${skill} install β€” refusing to delete." >&2
continue
fi
rm -rf "$dest"
echo " βœ… removed ${dest}"
any=1
done
[ "$any" = 1 ] || echo " (nothing removed)"
echo " Note: this does NOT touch any CLAUDE.md / .claude/rules / .claude/docmap.json that"
echo " /docmap generated in your repositories β€” use /undocmap or delete those per-repo."
exit 0
fi
# --- install ------------------------------------------------------------------
echo "==> Installing docmap + undocmap skills v${VERSION} to: ${SKILLS_ROOT}"
# Fetch the whole gist once (git clone), fall back to per-file curl.
TMP=""
cleanup() { [ -n "$TMP" ] && rm -rf "$TMP" || true; }
trap cleanup EXIT
fetch_all_via_git() {
command -v git >/dev/null 2>&1 || return 1
TMP="$(mktemp -d)"
git clone --depth 1 --quiet "$GIST_GIT" "$TMP" 2>/dev/null || return 1
return 0
}
get_file() { # get_file <gist-name> <out-path>
local name="$1" out="$2"
if [ -n "$TMP" ] && [ -f "$TMP/$name" ]; then
cp "$TMP/$name" "$out"; return 0
fi
command -v curl >/dev/null 2>&1 || return 1
curl -fsSL "${GIST_RAW}/${name}" -o "$out" || return 1
}
fetch_all_via_git || echo " (git clone unavailable β€” falling back to curl per file)"
for skill in "${SKILLS[@]}"; do
dest="${SKILLS_ROOT}/${skill}"
mkdir -p "$dest"
echo " - ${skill}"
for pair in $(declare_files "$skill"); do
src="${pair%%:*}"; dst="${pair##*:}"
get_file "$src" "$dest/$dst" || { echo "ERROR: failed to fetch $src" >&2; exit 1; }
[ -s "$dest/$dst" ] || { echo "ERROR: $dest/$dst is empty" >&2; exit 1; }
done
# version stamp per skill
INSTALLED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo unknown)"
printf 'docmap-suite v%s\ninstalled: %s\nsource: https://gist.github.com/Kinark/%s\n' \
"$VERSION" "$INSTALLED_AT" "$GIST_ID" > "$dest/$STAMP"
# optional syntax check (non-fatal)
if command -v node >/dev/null 2>&1; then
node --check "$dest/${skill}.workflow.js" 2>/dev/null \
&& echo " workflow syntax OK" \
|| echo " ! node --check flagged ${skill}.workflow.js (may still run under Claude Code)" >&2
fi
done
cat <<EOF
βœ… Installed docmap + undocmap v${VERSION} under: ${SKILLS_ROOT}
docmap/SKILL.md docmap/docmap.workflow.js
undocmap/SKILL.md undocmap/undocmap.workflow.js
Next steps:
1. Open Claude Code in any git repository.
2. Build/refresh the map: /docmap (auto-detects build vs update)
3. Tear it all down: /undocmap (destructive, no confirmation)
4. Uninstall these skills: re-run with -u (add --project for a repo-level install)
⚠️ Both are OVERRIDE tools:
β€’ /docmap OWNS every CLAUDE.md it writes β€” pre-existing CLAUDE.md files are treated as a
dispensable hint and overwritten.
β€’ /undocmap DELETES every CLAUDE.md docmap owns, its rules, and the manifest β€” no confirmation.
Keep anything you can't lose in CLAUDE.local.md (never touched) or commit it to git first.
$( [ "$SCOPE" = "user" ] && echo "User-level install β€” /docmap and /undocmap are now available in every project." \
|| echo "Project-level install β€” /docmap and /undocmap are available in this repo only." )
EOF
DOCMAP_INSTALLER_EOF
# Don't leave our temp vars behind in the sourcing shell.
unset _docmap_args _a 2>/dev/null || true
name undocmap
description DESTRUCTIVE teardown of the guidance map /docmap generated for the current codebase β€” deletes every per-folder CLAUDE.md, the root CLAUDE.md, all path-scoped .claude/rules/ files docmap created, and the .claude/docmap.json manifest. Runs without a confirmation prompt; the deletion is irreversible. Fully sub-agent driven via a Workflow. Use when the user asks to "remove the docmap", "undo docmap", "delete the generated CLAUDE.md files", "tear down agent guidance", or "clean up .claude/rules".

undocmap

The inverse of /docmap. This skill is a dispatcher β€” all work runs inside a deterministic Workflow whose steps are sub-agents on pinned models.

⚠️ Override / destructive tool β€” warn the user first

undocmap deletes files with no confirmation gate: the root CLAUDE.md, every per-folder CLAUDE.md docmap owns, docmap's .claude/rules/ files, and the .claude/docmap.json manifest. Deletion is irreversible (files aren't backed up).

Before launching the workflow, tell the user plainly what will be deleted and that it can't be undone, in the same turn. You don't need a formal yes/no gate, but the warning must be visible β€” undocmap is a teardown tool and treats these CLAUDE.md files as docmap-owned. If the user only wants some of it gone, do that manually instead of running this.

What to do

State the warning, then launch:

Workflow({ scriptPath: "<home>/.claude/skills/undocmap/undocmap.workflow.js" })

(Expand ~ to the current user's home, or use <repo>/.claude/skills/undocmap/... for a project-level install.) The user invoking this skill is the opt-in for the Workflow tool. Relay the returned summary (what was deleted, any warnings) afterward.

What the workflow does

  1. Inventory (Sonnet 5, read-only) β€” lists every artifact /docmap created, preferring .claude/docmap.json (the root + every unit CLAUDE.md, rule files, rule dirs). With no manifest it falls back to scanning docmap's known shape and flags inferred items in warnings.
  2. Remove (Sonnet 5) β€” deletes every listed CLAUDE.md and rule file, rmdirs rule dirs left empty, and deletes the manifest. It never touches .claude/ itself, settings, or CLAUDE.local.md.

If no artifacts are found it reports "nothing to remove" and exits.

Companion: docmap is also an override tool

/docmap owns every CLAUDE.md it writes β€” it reads a pre-existing CLAUDE.md only as a dispensable hint, then overwrites it. So the pair is symmetric: docmap overwrites, undocmap deletes. Neither preserves arbitrary hand-written CLAUDE.md content; keep anything you can't lose elsewhere (e.g. CLAUDE.local.md, which neither tool touches).

If the Workflow tool is unavailable

Fall back to the Agent tool with the same contract: a read-only Sonnet 5 inventory (manifest-first, filesystem fallback), then a Sonnet 5 remove pass that deletes exactly the inventoried artifacts. Warn the user before deleting.

export const meta = {
name: 'undocmap',
description: 'DESTRUCTIVE teardown of the guidance map /docmap generated: deletes every per-folder CLAUDE.md, the root CLAUDE.md, all path-scoped .claude/rules/ files docmap created, and the .claude/docmap.json manifest β€” no confirmation prompt. The caller must warn the user first. Fully sub-agent driven.',
phases: [
{ title: 'Inventory', detail: 'From the manifest (+ filesystem fallback), list every artifact docmap created', model: 'sonnet' },
{ title: 'Remove', detail: 'Delete every listed artifact', model: 'sonnet' },
],
}
// ---- schemas -----------------------------------------------------------------
const INVENTORY_SCHEMA = {
type: 'object', additionalProperties: false,
required: ['hasManifest', 'repoRoot', 'claudeFiles', 'ruleFiles', 'ruleDirs', 'manifestPath', 'warnings'],
properties: {
hasManifest: { type: 'boolean' },
repoRoot: { type: 'string' },
manifestPath: { type: 'string', description: 'path to .claude/docmap.json, or "" if none found' },
claudeFiles: {
type: 'array', items: { type: 'string' },
description: 'ALL repo-relative CLAUDE.md files docmap owns β€” the root plus every <unit>/CLAUDE.md',
},
ruleFiles: {
type: 'array', items: { type: 'string' },
description: 'repo-relative .claude/rules/*.md files docmap created',
},
ruleDirs: {
type: 'array', items: { type: 'string' },
description: 'repo-relative rule subdirs (e.g. .claude/rules/<unit>) that will be empty after removal',
},
warnings: { type: 'array', items: { type: 'string' }, description: 'anything ambiguous or skipped for safety' },
},
}
const REMOVE_SCHEMA = {
type: 'object', additionalProperties: false,
required: ['deleted', 'manifestDeleted', 'errors'],
properties: {
deleted: { type: 'array', items: { type: 'string' }, description: 'files/dirs actually removed' },
manifestDeleted: { type: 'boolean' },
errors: { type: 'array', items: { type: 'string' } },
},
}
// ---- prompts -----------------------------------------------------------------
const inventoryPrompt = () => `You are the INVENTORY scout for /undocmap. Work READ-ONLY; delete NOTHING.
Goal: list every artifact /docmap generated so it can be removed. docmap OWNS the CLAUDE.md files it
writes (root + per-unit), so those are all fair game for deletion.
repoRoot = \`git rev-parse --show-toplevel\` (fallback to cwd; note it in warnings).
STEP 1 - Prefer the manifest ".claude/docmap.json":
- If present -> hasManifest=true, manifestPath set.
- claudeFiles = the root CLAUDE.md (root.file, or ./CLAUDE.md / ./.claude/CLAUDE.md) PLUS
"<unit>/CLAUDE.md" for each path in units[] β€” every one that exists on disk.
- ruleFiles = every rules[].file that exists on disk.
- ruleDirs = distinct parent dirs under .claude/rules/ that will be left empty once ruleFiles are
gone (include .claude/rules/ itself only if it would end up fully empty).
STEP 2 - Filesystem fallback (hasManifest=false, no manifest):
- Be reasonable but note inference in warnings. docmap's shape is: a root CLAUDE.md, per-top-folder
CLAUDE.md files, and .claude/rules/*.md.
- claudeFiles = the root CLAUDE.md + every CLAUDE.md found in immediate top-level folders (and, for
a monorepo, one level into packages/apps/services/libs). Add a warning that these are inferred and
a hand-written CLAUDE.md would also match.
- ruleFiles = all .md under .claude/rules/. Warn that hand-authored rules would also live here.
- ruleDirs as above.
- Do NOT recurse into node_modules, dist, build, target, .venv, vendor, .git, etc.
Return the structured inventory. Every path repo-relative. List CLAUDE.local.md is NOT docmap's β€”
never include it. Never include settings files or .claude/ itself.`
const removePrompt = (inv) => `You are the REMOVE worker for /undocmap. Execute this teardown exactly. Use Bash (rm / rmdir).
Delete nothing outside this plan.
PLAN:
${JSON.stringify({ claudeFiles: inv.claudeFiles, ruleFiles: inv.ruleFiles, ruleDirs: inv.ruleDirs, manifestPath: inv.manifestPath }, null, 2)}
Steps:
1. Delete every file in claudeFiles (root + per-unit CLAUDE.md) and every file in ruleFiles. Skip any
already gone; record each removal in deleted[].
2. Remove each dir in ruleDirs ONLY if it is now empty (\`rmdir\`, never \`rm -rf\`). Remove
.claude/rules/ too if it ends up empty.
3. Delete the manifest at manifestPath if it exists; set manifestDeleted accordingly.
4. Do NOT touch .claude/ itself, settings files, CLAUDE.local.md, or anything not listed above.
Report exactly what you did (deleted[], manifestDeleted, errors[]).`
// ---- orchestration -----------------------------------------------------------
phase('Inventory')
const inv = await agent(inventoryPrompt(), {
label: 'inventory', phase: 'Inventory', model: 'sonnet', agentType: 'general-purpose', schema: INVENTORY_SCHEMA,
})
if (!inv) return { skipped: true, reason: 'inventory stage failed' }
const totalFiles = (inv.claudeFiles?.length || 0) + (inv.ruleFiles?.length || 0) + (inv.manifestPath ? 1 : 0)
if (totalFiles === 0) {
log('Nothing to remove β€” no docmap artifacts found.')
return { removed: false, nothingToRemove: true, hasManifest: inv.hasManifest, warnings: inv.warnings || [] }
}
log(`⚠️ Deleting ${totalFiles} docmap artifact(s) β€” every CLAUDE.md docmap owns, its rules, and the ` +
`manifest. This is irreversible; the caller was expected to warn the user first.`)
// No confirmation gate: /undocmap is an override/teardown tool by design.
phase('Remove')
const result = await agent(removePrompt(inv), {
label: 'remove', phase: 'Remove', model: 'sonnet', agentType: 'general-purpose', schema: REMOVE_SCHEMA,
})
return { removed: true, ...result, warnings: inv.warnings || [] }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment