Browse Source

fix(installer): stop duplicating agent instructions; MCP server is the single source of truth (#529) (#538)

The installer wrote a `## CodeGraph` usage block into each agent's
instructions file (CLAUDE.md / AGENTS.md / GEMINI.md / .cursor/rules /
Kiro steering) that duplicated, almost verbatim, the guidance the MCP
server already emits in its `initialize` response — so agents that
surface MCP instructions (Claude Code) read the same playbook twice
every turn.

All 6 instruction-writing targets (claude, cursor, codex, opencode,
gemini, kiro) now stop writing the block. install self-heals by
stripping a block a previous version wrote (uninstall already did), so
the next `codegraph install`/`uninstall` cleans up existing installs;
upgrading the package alone does not (the leftover block is harmless).
server-instructions.ts is now the single source of truth — the two
steers unique to the old template ("trust codegraph, don't re-verify
with grep" and the not-initialized -> `init -i` hint) are ported there.

Removes the now-dead INSTRUCTIONS_TEMPLATE / CLAUDE_MD_TEMPLATE,
claude-md-template.ts, writeClaudeMd / hasClaudeMdSection, and the
Cursor-only wireProjectSurfaces bootstrap. The install log learned a
"Removed" verb. Tests rewritten to the new contract + self-heal
coverage (140/140 installer tests pass).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry 3 tuần trước cách đây
mục cha
commit
a9c9e76d8c

+ 18 - 0
CHANGELOG.md

@@ -154,6 +154,24 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
   bodies). An agent investigating `Splitter.SplittingIterator.separatorStart`
   now sees the four anonymous overrides in its trail without a Read.
 
+### Changed
+- **The installer no longer writes a `## CodeGraph` instructions block into
+  your agent's instructions file** (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`,
+  Cursor's `.cursor/rules/codegraph.mdc`, or Kiro's steering doc). That block
+  duplicated, almost verbatim, the usage guidance the MCP server already
+  emits in its `initialize` response — so every agent that surfaces MCP
+  instructions (Claude Code does) read the same playbook twice each turn
+  (#529). The MCP server instructions are now the single source of truth.
+  `codegraph install` stops writing the block, and **the next time you run
+  `codegraph install` (or `codegraph uninstall`) it strips a block a previous
+  version wrote**, preserving everything else in the file (and deleting Cursor
+  `.mdc` / Kiro steering files that were ours outright). Note: simply upgrading
+  the npm package does not remove an existing block — re-run the installer to
+  clean it up. The leftover block is harmless meanwhile (just redundant with
+  the MCP instructions). If you'd added your own notes inside the
+  `<!-- CODEGRAPH_START -->`/`<!-- CODEGRAPH_END -->` markers, move them outside
+  the markers first — only the marked block is removed.
+
 ### Fixed
 - **MCP tools no longer return rows for files deleted while no server was
   running.** The post-open catch-up sync that reconciles the index against

+ 4 - 5
CLAUDE.md

@@ -74,12 +74,11 @@ Defined in `src/types.ts`. Both extractors and resolvers must use these exact st
 `src/installer/` is the entry point for `codegraph install` (and the bare `codegraph`/`npx @colbymchenry/codegraph` invocation). Architecture:
 
 - `targets/registry.ts` lists every supported agent.
-- `targets/types.ts` defines the `AgentTarget` interface — adding a 5th agent (Continue, Zed, Windsurf…) is **one new file in `targets/` + one entry in `registry.ts`**. Each target owns its config-file location, MCP-server JSON/TOML/JSONC writing, and instructions-file path.
+- `targets/types.ts` defines the `AgentTarget` interface — adding a 5th agent (Continue, Zed, Windsurf…) is **one new file in `targets/` + one entry in `registry.ts`**. Each target owns its config-file location and MCP-server JSON/TOML/JSONC writing. (Targets no longer write an instructions file — see below.)
 - Current targets: `claude.ts`, `cursor.ts`, `codex.ts`, `opencode.ts`.
 - `targets/toml.ts` is a hand-rolled TOML serializer scoped to `[mcp_servers.codegraph]` (used by Codex). Sibling tables and `[[array_of_tables]]` are preserved verbatim. No new dependency.
 - opencode reads `opencode.jsonc` by default; the installer prefers existing `.jsonc`, falls back to `.json`, and creates `.jsonc` for greenfield installs. Edits are surgical via `jsonc-parser` so user comments and formatting survive install/re-install/uninstall round-trips.
-- `instructions-template.ts` is the agent-agnostic instructions file written to each target (e.g. `CLAUDE.md`, `.cursor/rules/codegraph.mdc`, `~/.codex/AGENTS.md`, `~/.config/opencode/AGENTS.md`). It explicitly says "trust codegraph results, don't re-verify with grep" — earlier versions prescribed Claude-specific "spawn an Explore agent" and confused other agents.
-- `claude-md-template.ts` is the legacy Claude-only template, retained for compatibility paths.
+- `instructions-template.ts` no longer holds an instructions body — it exports only the `<!-- CODEGRAPH_START -->`/`<!-- CODEGRAPH_END -->` markers. The installer **stopped writing** a `## CodeGraph` block into each agent's instructions file (`CLAUDE.md` / `~/.codex/AGENTS.md` / `~/.config/opencode/AGENTS.md` / `~/.gemini/GEMINI.md` / `.cursor/rules/codegraph.mdc` / Kiro steering doc) because it duplicated the MCP `initialize` instructions verbatim (issue #529). Each target's `install` (self-heal on upgrade) and `uninstall` use the markers to **strip** a block a previous install left behind. `server-instructions.ts` is the single source of truth for agent-facing guidance.
 - All installer changes need matching coverage in `__tests__/installer-targets.test.ts` — there are ~47 parameterized contract tests covering install idempotency, sibling preservation, uninstall reverses install, byte-equal re-runs returning `unchanged`, and partial-state recovery for Codex.
 
 ### Cursor MCP working-directory quirk
@@ -88,7 +87,7 @@ Cursor launches MCP subprocesses with the wrong cwd and doesn't pass `rootUri` i
 
 ### MCP server instructions
 
-`src/mcp/server-instructions.ts` is sent back to the agent in the MCP `initialize` response. This is the *first* thing every agent sees about how to use the tools — treat it as the authoritative tool guidance and keep it in sync with `instructions-template.ts` and `.cursor/rules/codegraph.mdc`.
+`src/mcp/server-instructions.ts` is sent back to the agent in the MCP `initialize` response. This is the *first* thing every agent sees about how to use the tools, and as of issue #529 it is the **single source of truth** for agent-facing tool guidance — the installer no longer writes a duplicate `## CodeGraph` instructions block into `CLAUDE.md` / `AGENTS.md` / `.cursor/rules/codegraph.mdc`. Edit tool guidance here and nowhere else.
 
 ## Retrieval performance & dynamic-dispatch coverage (do not regress)
 
@@ -254,7 +253,7 @@ publish actions on shared state. Write the files, hand the user the commands.
 ## House rules
 
 - The `0.7.x` line is in active multi-agent rollout. Any change to `src/installer/` (especially `targets/`) needs corresponding test coverage and a CHANGELOG entry — installer regressions break every new install silently.
-- When changing what the MCP tools do or how agents should use them, update **all three** of `src/mcp/server-instructions.ts`, `src/installer/instructions-template.ts`, and `.cursor/rules/codegraph.mdc` — they're written to different places but say the same thing.
+- When changing what the MCP tools do or how agents should use them, edit `src/mcp/server-instructions.ts` — it is the **single source of truth** for agent-facing tool guidance (issue #529). The installer no longer writes a duplicate instructions block into `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `.cursor/rules/codegraph.mdc` / Kiro steering, so there's nothing to keep in sync anymore. (The repo's own checked-in `.cursor/rules/codegraph.mdc` is dogfooding config — update it too if you use Cursor on this repo, but it ships nowhere.)
 - CodeGraph provides **code context**, not product requirements. For new features, ask the user about UX, edge cases, and acceptance criteria — the graph won't tell you.
 - **When the user references issues, PR comments, or external reports, anchor them to a date and version before drawing conclusions.** Check the comment's `createdAt` against:
   - The **last released version** — `grep -m1 '^## \[' CHANGELOG.md` shows the top-of-file version (older releases follow). A comment dated before the latest `## [X.Y.Z] - YYYY-MM-DD` is reacting to *released* state — work that's only on `main` or on an unmerged branch doesn't apply.

+ 11 - 33
README.md

@@ -236,7 +236,7 @@ The installer will:
 - Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**
 - Prompt to install `codegraph` on your PATH (so agents can launch the MCP server)
 - Ask whether configs apply to all your projects or just this one
-- Write each chosen agent's MCP server config + an instructions file (e.g. `CLAUDE.md`, `.cursor/rules/codegraph.mdc`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`)
+- Write each chosen agent's MCP server config (the codegraph usage guide is delivered by the MCP server itself, so no instructions file is added to `CLAUDE.md` / `AGENTS.md` / etc.)
 - Set up auto-allow permissions when Claude Code is one of the targets
 - Initialize your current project (local installs only)
 
@@ -268,7 +268,7 @@ cd your-project
 codegraph init -i
 ```
 
-Builds the per-project knowledge graph index. Also wires up any project-local agent surfaces (e.g. Cursor's `.cursor/rules/codegraph.mdc`) so a single global `codegraph install` works in every project you open — no need to re-run the installer per project.
+Builds the per-project knowledge graph index. A single global `codegraph install` works in every project you open — no need to re-run the installer per project.
 
 That's it — your agent will use CodeGraph tools automatically when a `.codegraph/` directory exists.
 
@@ -314,39 +314,16 @@ npm install -g @colbymchenry/codegraph
 </details>
 
 <details>
-<summary><strong>Global Instructions Reference</strong></summary>
+<summary><strong>Agent Tool Guidance</strong></summary>
 
-The installer automatically adds these instructions to `~/.claude/CLAUDE.md`:
+CodeGraph's MCP server delivers its usage guidance to your agent **automatically**, in the MCP `initialize` response — there's no instructions file to manage and nothing is added to your `CLAUDE.md` / `AGENTS.md` / `GEMINI.md`. In short, it tells the agent to:
 
-```markdown
-## CodeGraph
+- **Answer structural questions directly with CodeGraph** — it *is* the pre-built index, so a grep/read loop just repeats work it already did. Treat the returned source as already read.
+- **Pick the tool by intent:** `codegraph_context` to map an area, `codegraph_trace` for "how does X reach Y", `codegraph_explore` to survey several symbols, `codegraph_search` to find a symbol, `codegraph_callers`/`codegraph_callees` to walk call flow, `codegraph_impact` before editing, `codegraph_node` for one symbol's source.
+- **Trust the results — don't re-verify with grep**, and check the staleness banner after edits.
+- If `.codegraph/` doesn't exist yet, offer to run `codegraph init -i`.
 
-CodeGraph builds a semantic knowledge graph of codebases for faster, smarter code exploration.
-
-### If `.codegraph/` exists in the project
-
-**Answer directly with CodeGraph — don't delegate exploration to a file-reading sub-agent or a grep/read loop.** CodeGraph *is* the pre-built search index; re-deriving its answers with grep + Read repeats work it already did and costs more for the same result. For "how does X work?", architecture, trace, or where-is-X questions, answer in a handful of CodeGraph calls and stop — typically with **zero file reads**. The returned source is complete and authoritative: treat it as already read and do not re-open those files. Reach for raw Read/Grep only to confirm a specific detail CodeGraph didn't cover.
-
-**Tool selection by intent:**
-
-| Tool | Use For |
-|------|---------|
-| `codegraph_context` | Map a task / feature / area first — composes search + node + callers + callees in one call |
-| `codegraph_trace` | "How does X reach Y" — the call path, each hop's body inline (follows dynamic-dispatch hops grep can't) |
-| `codegraph_explore` | Survey several related symbols' source in ONE budget-capped call |
-| `codegraph_search` | Find a symbol by name |
-| `codegraph_callers` / `codegraph_callees` | Walk call flow one hop at a time |
-| `codegraph_impact` | Check what's affected before editing |
-| `codegraph_node` | Get a single symbol's source / signature |
-
-A direct CodeGraph answer is a handful of calls; a grep/read exploration is dozens.
-
-### If `.codegraph/` does NOT exist
-
-At the start of a session, ask the user if they'd like to initialize CodeGraph:
-
-"I notice this project doesn't have CodeGraph initialized. Would you like me to run `codegraph init -i` to build a code knowledge graph?"
-```
+The exact text is `src/mcp/server-instructions.ts` — the single source of truth.
 
 </details>
 
@@ -517,7 +494,8 @@ See [Get Started](#get-started) for the one-line install commands.
 ## Supported Agents
 
 The interactive installer auto-detects and configures each of these — wiring up
-the MCP server and writing its instructions file:
+the MCP server (which delivers its own usage guidance, so no instructions file
+is written):
 
 - **Claude Code**
 - **Cursor**

+ 133 - 65
__tests__/installer-targets.test.ts

@@ -55,6 +55,18 @@ function setHome(dir: string): { restore: () => void } {
   };
 }
 
+// A marker-delimited CodeGraph block exactly as a previous installer
+// wrote it. Issue #529: the installer no longer writes an instructions
+// file, but install (self-heal on upgrade) and uninstall both still
+// strip a block a prior install left, so we plant this to exercise it.
+const LEGACY_BLOCK = [
+  '<!-- CODEGRAPH_START -->',
+  '## CodeGraph',
+  '',
+  'Prefer `codegraph_search` / `codegraph_callers` over grep.',
+  '<!-- CODEGRAPH_END -->',
+].join('\n');
+
 describe('Installer targets — contract', () => {
   let tmpHome: string;
   let tmpCwd: string;
@@ -180,23 +192,35 @@ describe('Installer targets — partial-state idempotency', () => {
     fs.rmSync(tmpCwd, { recursive: true, force: true });
   });
 
-  it('codex: install after only config.toml exists — second pass is fully unchanged', () => {
+  it('codex: install writes config.toml but never an AGENTS.md instructions file (#529)', () => {
     const codex = getTarget('codex')!;
-    // First install creates both files.
-    codex.install('global', { autoAllow: false });
-    // Delete the AGENTS.md to simulate partial state (user wiped one file).
+    const first = codex.install('global', { autoAllow: false });
     const agentsMd = path.join(tmpHome, '.codex', 'AGENTS.md');
-    expect(fs.existsSync(agentsMd)).toBe(true);
-    fs.unlinkSync(agentsMd);
-    // Reinstall — TOML stays unchanged, AGENTS.md is recreated.
+    // No instructions file is created, and no file action references it.
+    expect(fs.existsSync(agentsMd)).toBe(false);
+    expect(first.files.some((f) => f.path.endsWith('AGENTS.md'))).toBe(false);
+    expect(first.files.some((f) => f.path.endsWith('config.toml'))).toBe(true);
+    // Re-install is fully unchanged (config.toml only, nothing to strip).
     const second = codex.install('global', { autoAllow: false });
-    const tomlEntry = second.files.find((f) => f.path.endsWith('config.toml'))!;
-    const mdEntry = second.files.find((f) => f.path.endsWith('AGENTS.md'))!;
-    expect(tomlEntry.action).toBe('unchanged');
-    expect(mdEntry.action).toBe('created');
-    // Third install — both unchanged (full idempotency restored).
-    const third = codex.install('global', { autoAllow: false });
-    for (const f of third.files) expect(f.action).toBe('unchanged');
+    for (const f of second.files) expect(f.action).toBe('unchanged');
+  });
+
+  it('codex: install strips a legacy AGENTS.md codegraph block, keeping user content (#529)', () => {
+    const codex = getTarget('codex')!;
+    const dir = path.join(tmpHome, '.codex');
+    fs.mkdirSync(dir, { recursive: true });
+    const agentsMd = path.join(dir, 'AGENTS.md');
+    fs.writeFileSync(agentsMd, `# My codex notes\n\nBe terse.\n\n${LEGACY_BLOCK}\n`);
+
+    const result = codex.install('global', { autoAllow: false });
+
+    const body = fs.readFileSync(agentsMd, 'utf-8');
+    expect(body).toContain('# My codex notes');
+    expect(body).toContain('Be terse.');
+    expect(body).not.toContain('CODEGRAPH_START');
+    // The strip is reported as a 'removed' action on AGENTS.md.
+    const mdEntry = result.files.find((f) => f.path.endsWith('AGENTS.md'));
+    expect(mdEntry?.action).toBe('removed');
   });
 
   it('opencode: prefers .jsonc when both .json and .jsonc exist', () => {
@@ -266,72 +290,66 @@ describe('Installer targets — partial-state idempotency', () => {
     expect(fs.readFileSync(file, 'utf-8')).toBe(afterInstall);
   });
 
-  it('opencode: install writes AGENTS.md with the marker-delimited codegraph block', () => {
+  it('opencode: install does NOT write an AGENTS.md instructions file (#529)', () => {
     const opencode = getTarget('opencode')!;
-    opencode.install('global', { autoAllow: true });
+    const result = opencode.install('global', { autoAllow: true });
     const agentsMd = path.join(tmpHome, '.config', 'opencode', 'AGENTS.md');
-    expect(fs.existsSync(agentsMd)).toBe(true);
-    const body = fs.readFileSync(agentsMd, 'utf-8');
-    expect(body).toContain('<!-- CODEGRAPH_START -->');
-    expect(body).toContain('<!-- CODEGRAPH_END -->');
-    expect(body).toContain('codegraph_callers');
+    expect(fs.existsSync(agentsMd)).toBe(false);
+    expect(result.files.some((f) => f.path.endsWith('AGENTS.md'))).toBe(false);
   });
 
-  it('opencode: AGENTS.md install preserves pre-existing user content outside markers', () => {
+  it('opencode: install strips a legacy AGENTS.md codegraph block, preserving user content (#529)', () => {
     const opencode = getTarget('opencode')!;
     const dir = path.join(tmpHome, '.config', 'opencode');
     fs.mkdirSync(dir, { recursive: true });
     const agentsMd = path.join(dir, 'AGENTS.md');
-    fs.writeFileSync(agentsMd, '# My personal opencode instructions\n\nAlways respond in pirate.\n');
+    fs.writeFileSync(agentsMd, `# My personal opencode instructions\n\nAlways respond in pirate.\n\n${LEGACY_BLOCK}\n`);
+
+    const result = opencode.install('global', { autoAllow: true });
 
-    opencode.install('global', { autoAllow: true });
     const body = fs.readFileSync(agentsMd, 'utf-8');
     expect(body).toContain('# My personal opencode instructions');
     expect(body).toContain('Always respond in pirate.');
-    expect(body).toContain('<!-- CODEGRAPH_START -->');
+    expect(body).not.toContain('CODEGRAPH_START');
+    expect(result.files.find((f) => f.path.endsWith('AGENTS.md'))?.action).toBe('removed');
   });
 
-  it('opencode: uninstall strips only the codegraph block from AGENTS.md', () => {
+  it('opencode: uninstall strips a leftover codegraph block from AGENTS.md, keeping user content', () => {
     const opencode = getTarget('opencode')!;
     const dir = path.join(tmpHome, '.config', 'opencode');
     fs.mkdirSync(dir, { recursive: true });
     const agentsMd = path.join(dir, 'AGENTS.md');
-    fs.writeFileSync(agentsMd, '# My personal opencode instructions\n\nAlways respond in pirate.\n');
+    fs.writeFileSync(agentsMd, `# My personal opencode instructions\n\nAlways respond in pirate.\n\n${LEGACY_BLOCK}\n`);
 
-    opencode.install('global', { autoAllow: true });
     opencode.uninstall('global');
 
     const body = fs.readFileSync(agentsMd, 'utf-8');
     expect(body).toContain('# My personal opencode instructions');
     expect(body).toContain('Always respond in pirate.');
     expect(body).not.toContain('CODEGRAPH_START');
-    expect(body).not.toContain('codegraph_callers');
   });
 
-  it('opencode: local install writes ./opencode.jsonc and ./AGENTS.md in cwd', () => {
+  it('opencode: local install writes ./opencode.jsonc and never an ./AGENTS.md (#529)', () => {
     const opencode = getTarget('opencode')!;
     const result = opencode.install('local', { autoAllow: true });
     const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
     // macOS realpath shenanigans (/var vs /private/var) — suffix match.
     expect(paths.some((p) => p.endsWith('/opencode.jsonc'))).toBe(true);
-    expect(paths.some((p) => p.endsWith('/AGENTS.md'))).toBe(true);
+    expect(paths.some((p) => p.endsWith('/AGENTS.md'))).toBe(false);
+    expect(fs.existsSync(path.join(process.cwd(), 'AGENTS.md'))).toBe(false);
   });
 
-  it('gemini: install writes settings.json (mcpServers.codegraph) and GEMINI.md with marker block', () => {
+  it('gemini: install writes settings.json (mcpServers.codegraph) and no GEMINI.md (#529)', () => {
     const gemini = getTarget('gemini')!;
     const result = gemini.install('global', { autoAllow: true });
     const settings = path.join(tmpHome, '.gemini', 'settings.json');
     const geminiMd = path.join(tmpHome, '.gemini', 'GEMINI.md');
     expect(result.files.some((f) => f.path === settings)).toBe(true);
-    expect(result.files.some((f) => f.path === geminiMd)).toBe(true);
+    expect(result.files.some((f) => f.path === geminiMd)).toBe(false);
+    expect(fs.existsSync(geminiMd)).toBe(false);
 
     const cfg = JSON.parse(fs.readFileSync(settings, 'utf-8'));
     expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] });
-
-    const md = fs.readFileSync(geminiMd, 'utf-8');
-    expect(md).toContain('<!-- CODEGRAPH_START -->');
-    expect(md).toContain('<!-- CODEGRAPH_END -->');
-    expect(md).toContain('codegraph_callers');
   });
 
   it('gemini: install preserves pre-existing settings (security.auth survives)', () => {
@@ -365,45 +383,51 @@ describe('Installer targets — partial-state idempotency', () => {
     expect(after.mcpServers).toBeUndefined();
   });
 
-  it('gemini: local install writes ./.gemini/settings.json and ./GEMINI.md (project root)', () => {
+  it('gemini: local install writes ./.gemini/settings.json and never a ./GEMINI.md (#529)', () => {
     const gemini = getTarget('gemini')!;
     const result = gemini.install('local', { autoAllow: true });
     const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
     expect(paths.some((p) => p.endsWith('/.gemini/settings.json'))).toBe(true);
-    // Local GEMINI.md sits at the project root, NOT under .gemini/.
-    expect(paths.some((p) => p.endsWith('/GEMINI.md') && !p.endsWith('/.gemini/GEMINI.md'))).toBe(true);
+    expect(paths.some((p) => p.endsWith('/GEMINI.md'))).toBe(false);
+    expect(fs.existsSync(path.join(process.cwd(), 'GEMINI.md'))).toBe(false);
   });
 
-  it('gemini: GEMINI.md uninstall preserves user content outside the codegraph markers', () => {
+  it('gemini: uninstall strips a leftover GEMINI.md codegraph block, keeping user content', () => {
     const gemini = getTarget('gemini')!;
     const geminiMd = path.join(tmpHome, '.gemini', 'GEMINI.md');
     fs.mkdirSync(path.dirname(geminiMd), { recursive: true });
-    fs.writeFileSync(geminiMd, '# My personal Gemini context\n\nAlways respond concisely.\n');
+    fs.writeFileSync(geminiMd, `# My personal Gemini context\n\nAlways respond concisely.\n\n${LEGACY_BLOCK}\n`);
 
-    gemini.install('global', { autoAllow: true });
     gemini.uninstall('global');
 
     const body = fs.readFileSync(geminiMd, 'utf-8');
     expect(body).toContain('# My personal Gemini context');
     expect(body).toContain('Always respond concisely.');
     expect(body).not.toContain('CODEGRAPH_START');
-    expect(body).not.toContain('codegraph_callers');
   });
 
-  it('kiro: install writes settings/mcp.json (mcpServers.codegraph) and steering/codegraph.md', () => {
+  it('kiro: install writes settings/mcp.json (mcpServers.codegraph) and no steering doc (#529)', () => {
     const kiro = getTarget('kiro')!;
     const result = kiro.install('global', { autoAllow: true });
     const mcp = path.join(tmpHome, '.kiro', 'settings', 'mcp.json');
     const steering = path.join(tmpHome, '.kiro', 'steering', 'codegraph.md');
     expect(result.files.some((f) => f.path === mcp)).toBe(true);
-    expect(result.files.some((f) => f.path === steering)).toBe(true);
+    expect(result.files.some((f) => f.path === steering)).toBe(false);
+    expect(fs.existsSync(steering)).toBe(false);
 
     const cfg = JSON.parse(fs.readFileSync(mcp, 'utf-8'));
     expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] });
+  });
+
+  it('kiro: install deletes a leftover steering codegraph.md (self-heal) (#529)', () => {
+    const kiro = getTarget('kiro')!;
+    const steering = path.join(tmpHome, '.kiro', 'steering', 'codegraph.md');
+    fs.mkdirSync(path.dirname(steering), { recursive: true });
+    fs.writeFileSync(steering, `${LEGACY_BLOCK}\n`);
 
-    const md = fs.readFileSync(steering, 'utf-8');
-    expect(md).toContain('codegraph_callers');
-    expect(md).toContain('CodeGraph MCP server');
+    const result = kiro.install('global', { autoAllow: true });
+    expect(fs.existsSync(steering)).toBe(false);
+    expect(result.files.find((f) => f.path === steering)?.action).toBe('removed');
   });
 
   it('kiro: install preserves a pre-existing sibling MCP server in mcp.json', () => {
@@ -437,35 +461,37 @@ describe('Installer targets — partial-state idempotency', () => {
     expect(after.mcpServers.codegraph).toBeUndefined();
   });
 
-  it('kiro: uninstall removes the steering codegraph.md file outright', () => {
+  it('kiro: uninstall removes a leftover steering codegraph.md file outright', () => {
     const kiro = getTarget('kiro')!;
-    kiro.install('global', { autoAllow: true });
     const steering = path.join(tmpHome, '.kiro', 'steering', 'codegraph.md');
-    expect(fs.existsSync(steering)).toBe(true);
+    fs.mkdirSync(path.dirname(steering), { recursive: true });
+    fs.writeFileSync(steering, `${LEGACY_BLOCK}\n`);
 
     kiro.uninstall('global');
     expect(fs.existsSync(steering)).toBe(false);
   });
 
-  it('kiro: uninstall leaves a sibling steering file (product.md) untouched', () => {
+  it('kiro: uninstall removes our steering doc but leaves a sibling (product.md) untouched', () => {
     const kiro = getTarget('kiro')!;
     const sibling = path.join(tmpHome, '.kiro', 'steering', 'product.md');
+    const ours = path.join(tmpHome, '.kiro', 'steering', 'codegraph.md');
     fs.mkdirSync(path.dirname(sibling), { recursive: true });
     fs.writeFileSync(sibling, '# Product\n\nMy team practices.\n');
+    fs.writeFileSync(ours, `${LEGACY_BLOCK}\n`);
 
-    kiro.install('global', { autoAllow: true });
     kiro.uninstall('global');
 
+    expect(fs.existsSync(ours)).toBe(false);
     expect(fs.existsSync(sibling)).toBe(true);
     expect(fs.readFileSync(sibling, 'utf-8')).toContain('My team practices.');
   });
 
-  it('kiro: local install writes ./.kiro/settings/mcp.json and ./.kiro/steering/codegraph.md', () => {
+  it('kiro: local install writes ./.kiro/settings/mcp.json and no steering doc (#529)', () => {
     const kiro = getTarget('kiro')!;
     const result = kiro.install('local', { autoAllow: true });
     const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
     expect(paths.some((p) => p.endsWith('/.kiro/settings/mcp.json'))).toBe(true);
-    expect(paths.some((p) => p.endsWith('/.kiro/steering/codegraph.md'))).toBe(true);
+    expect(paths.some((p) => p.endsWith('/.kiro/steering/codegraph.md'))).toBe(false);
   });
 
   it('antigravity: install writes to LEGACY ~/.gemini/antigravity/mcp_config.json when no migration marker', () => {
@@ -854,6 +880,29 @@ describe('Installer targets — partial-state idempotency', () => {
     expect(cfg.mcpServers.codegraph).toBeDefined();
   });
 
+  it('claude: install does NOT create a CLAUDE.md instructions file (#529)', () => {
+    const claude = getTarget('claude')!;
+    const result = claude.install('local', { autoAllow: false });
+    const claudeMd = path.join(tmpCwd, '.claude', 'CLAUDE.md');
+    expect(fs.existsSync(claudeMd)).toBe(false);
+    expect(result.files.some((f) => f.path.endsWith('CLAUDE.md'))).toBe(false);
+  });
+
+  it('claude: install strips a legacy CLAUDE.md codegraph block, keeping user content (#529)', () => {
+    const claude = getTarget('claude')!;
+    const claudeMd = path.join(tmpCwd, '.claude', 'CLAUDE.md');
+    fs.mkdirSync(path.dirname(claudeMd), { recursive: true });
+    fs.writeFileSync(claudeMd, `# My project rules\n\nUse tabs.\n\n${LEGACY_BLOCK}\n`);
+
+    const result = claude.install('local', { autoAllow: false });
+
+    const body = fs.readFileSync(claudeMd, 'utf-8');
+    expect(body).toContain('# My project rules');
+    expect(body).toContain('Use tabs.');
+    expect(body).not.toContain('CODEGRAPH_START');
+    expect(result.files.find((f) => f.path.endsWith('CLAUDE.md'))?.action).toBe('removed');
+  });
+
   it('claude: global install targets ~/.claude.json (user scope)', () => {
     const claude = getTarget('claude')!;
     claude.install('global', { autoAllow: false });
@@ -1282,22 +1331,41 @@ describe('Installer — Cursor rules file cleanup on uninstall', () => {
 
   const rulesFile = () => path.join(process.cwd(), '.cursor', 'rules', 'codegraph.mdc');
 
-  it('deletes the dedicated codegraph.mdc entirely (no orphaned frontmatter left behind)', () => {
-    cursor.install('local', { autoAllow: true });
+  // The frontmatter a previous install wrote ahead of the marked block.
+  // `removeRulesEntry` recognizes it to decide whether the leftover .mdc
+  // is ours-to-delete or carries user content worth keeping.
+  const MDC_FRONTMATTER = [
+    '---',
+    'description: CodeGraph MCP usage guide — when to use which tool',
+    'alwaysApply: true',
+    '---',
+    '',
+  ].join('\n');
+
+  function plantLegacyRulesFile(extra = ''): void {
+    fs.mkdirSync(path.dirname(rulesFile()), { recursive: true });
+    fs.writeFileSync(rulesFile(), MDC_FRONTMATTER + LEGACY_BLOCK + '\n' + extra);
+  }
+
+  it('uninstall deletes a leftover codegraph.mdc entirely (no orphaned frontmatter left behind)', () => {
+    plantLegacyRulesFile();
     expect(fs.existsSync(rulesFile())).toBe(true);
 
     cursor.uninstall('local');
 
     // The whole file — frontmatter included — is gone, not just the block.
     expect(fs.existsSync(rulesFile())).toBe(false);
-    expect(cursor.detect('local').alreadyConfigured).toBe(false);
   });
 
-  it('preserves user content added outside the codegraph markers (strips only our block)', () => {
-    cursor.install('local', { autoAllow: true });
-    const withUserContent =
-      fs.readFileSync(rulesFile(), 'utf-8') + '\n## My own rule\nkeep me\n';
-    fs.writeFileSync(rulesFile(), withUserContent);
+  it('install self-heals a leftover codegraph.mdc (#529)', () => {
+    plantLegacyRulesFile();
+    const result = cursor.install('local', { autoAllow: true });
+    expect(fs.existsSync(rulesFile())).toBe(false);
+    expect(result.files.some((f) => f.path.endsWith('codegraph.mdc') && f.action === 'removed')).toBe(true);
+  });
+
+  it('uninstall preserves user content added outside the codegraph markers (strips only our block)', () => {
+    plantLegacyRulesFile('## My own rule\nkeep me\n');
 
     cursor.uninstall('local');
 

+ 4 - 120
__tests__/installer.test.ts

@@ -3,7 +3,10 @@
  *
  * Tests for installer config-writer fixes:
  * - readJsonFile error handling
- * - writeClaudeMd section replacement
+ *
+ * (The CLAUDE.md instructions block is no longer written — see issue
+ * #529. The marker-based install/uninstall self-heal is covered in
+ * `installer-targets.test.ts`.)
  */
 
 import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
@@ -14,11 +17,6 @@ import * as os from 'os';
 // We test the exported functions from config-writer
 import {
   writeMcpConfig,
-  writePermissions,
-  writeClaudeMd,
-  hasMcpConfig,
-  hasPermissions,
-  hasClaudeMdSection,
 } from '../src/installer/config-writer';
 
 function createTempDir(): string {
@@ -103,118 +101,4 @@ describe('Installer Config Writer', () => {
       expect(content.customField).toBe('preserved');
     });
   });
-
-  describe('writeClaudeMd section replacement', () => {
-    it('should create new CLAUDE.md with markers', () => {
-      const result = writeClaudeMd('local');
-
-      expect(result.created).toBe(true);
-      const content = fs.readFileSync(path.join(tempDir, '.claude', 'CLAUDE.md'), 'utf-8');
-      expect(content).toContain('<!-- CODEGRAPH_START -->');
-      expect(content).toContain('<!-- CODEGRAPH_END -->');
-      expect(content).toContain('## CodeGraph');
-    });
-
-    it('should replace marked section on update', () => {
-      // First write
-      writeClaudeMd('local');
-
-      // Modify file to add custom content before and after
-      const claudeMdPath = path.join(tempDir, '.claude', 'CLAUDE.md');
-      const original = fs.readFileSync(claudeMdPath, 'utf-8');
-      const modified = '## My Custom Section\n\nCustom content\n\n' + original + '\n\n## Another Section\n\nMore content\n';
-      fs.writeFileSync(claudeMdPath, modified);
-
-      // Second write should leave the marked block as-is (byte-identical
-      // body, so result is `created:false, updated:false` — both flags
-      // are off but the surrounding custom content must survive).
-      writeClaudeMd('local');
-
-      const final = fs.readFileSync(claudeMdPath, 'utf-8');
-      expect(final).toContain('## My Custom Section');
-      expect(final).toContain('Custom content');
-      expect(final).toContain('## Another Section');
-      expect(final).toContain('More content');
-      expect(final).toContain('## CodeGraph');
-    });
-
-    it('should use atomic writes (no temp files left behind)', () => {
-      writeClaudeMd('local');
-
-      const claudeDir = path.join(tempDir, '.claude');
-      const files = fs.readdirSync(claudeDir);
-      const tmpFiles = files.filter(f => f.includes('.tmp.'));
-      expect(tmpFiles).toHaveLength(0);
-    });
-
-    it('should not overwrite content after unmarked section with ### subsections', () => {
-      // Create a CLAUDE.md with an unmarked CodeGraph section that has ### subsections
-      // followed by another ## section
-      const claudeDir = path.join(tempDir, '.claude');
-      fs.mkdirSync(claudeDir, { recursive: true });
-      const claudeMdPath = path.join(claudeDir, 'CLAUDE.md');
-      fs.writeFileSync(claudeMdPath, [
-        '## Pre-existing Section',
-        '',
-        'Some content',
-        '',
-        '## CodeGraph',
-        '',
-        '### Subsection A',
-        '',
-        'Old codegraph content',
-        '',
-        '### Subsection B',
-        '',
-        'More old content',
-        '',
-        '## Important Section After',
-        '',
-        'This content must not be overwritten!',
-        '',
-      ].join('\n'));
-
-      const result = writeClaudeMd('local');
-      expect(result.updated).toBe(true);
-
-      const final = fs.readFileSync(claudeMdPath, 'utf-8');
-      // The section after CodeGraph must be preserved
-      expect(final).toContain('## Important Section After');
-      expect(final).toContain('This content must not be overwritten!');
-      // Pre-existing section should also be preserved
-      expect(final).toContain('## Pre-existing Section');
-      // New CodeGraph content should be present with markers
-      expect(final).toContain('<!-- CODEGRAPH_START -->');
-      expect(final).toContain('<!-- CODEGRAPH_END -->');
-    });
-
-    it('should replace unmarked section without subsections', () => {
-      const claudeDir = path.join(tempDir, '.claude');
-      fs.mkdirSync(claudeDir, { recursive: true });
-      const claudeMdPath = path.join(claudeDir, 'CLAUDE.md');
-      // Note: regex needs \n before ## CodeGraph, so prefix with another section
-      fs.writeFileSync(claudeMdPath, [
-        '## Intro',
-        '',
-        'Preamble',
-        '',
-        '## CodeGraph',
-        '',
-        'Old simple content',
-        '',
-        '## Next Section',
-        '',
-        'Must be preserved',
-        '',
-      ].join('\n'));
-
-      writeClaudeMd('local');
-
-      const final = fs.readFileSync(claudeMdPath, 'utf-8');
-      expect(final).toContain('<!-- CODEGRAPH_START -->');
-      expect(final).toContain('## Next Section');
-      expect(final).toContain('Must be preserved');
-      expect(final).not.toContain('Old simple content');
-    });
-  });
 });

+ 0 - 23
src/bin/codegraph.ts

@@ -429,15 +429,6 @@ program
       if (isInitialized(projectPath)) {
         clack.log.warn(`Already initialized in ${projectPath}`);
         clack.log.info('Use "codegraph index" to re-index or "codegraph sync" to update');
-        // Re-run agent surface wiring so re-running `init` is the
-        // documented way to recover a project that's missing its
-        // Cursor rules file (or future per-agent project surfaces).
-        try {
-          const { wireProjectSurfacesForGlobalAgents } = await import('../installer');
-          for (const { target, file } of wireProjectSurfacesForGlobalAgents()) {
-            clack.log.success(`${target.displayName}: ${file.action} ${file.path}`);
-          }
-        } catch { /* non-fatal */ }
         try {
           const { offerWatchFallback } = await import('../installer');
           await offerWatchFallback(clack, projectPath);
@@ -450,20 +441,6 @@ program
       const cg = await CodeGraph.init(projectPath, { index: false });
       clack.log.success(`Initialized in ${projectPath}`);
 
-      // Bootstrap project-local surfaces for any agent that's
-      // configured globally (Cursor needs ./.cursor/rules/codegraph.mdc
-      // to actually prefer codegraph over native grep). Silent when
-      // there's nothing to write.
-      try {
-        const { wireProjectSurfacesForGlobalAgents } = await import('../installer');
-        for (const { target, file } of wireProjectSurfacesForGlobalAgents()) {
-          clack.log.success(`${target.displayName}: ${file.action} ${file.path}`);
-        }
-      } catch (err) {
-        const msg = err instanceof Error ? err.message : String(err);
-        clack.log.warn(`Skipped wiring project-local agent surfaces: ${msg}`);
-      }
-
       if (options.index) {
         let result: IndexResult;
 

+ 0 - 19
src/installer/claude-md-template.ts

@@ -1,19 +0,0 @@
-/**
- * Backwards-compat re-export shim.
- *
- * The instructions template moved to `instructions-template.ts` so it
- * can be shared across all agent targets (Claude Code, Cursor, Codex
- * CLI, opencode). This file is preserved purely so existing imports
- * (`@colbymchenry/codegraph` consumers, downstream tooling) keep
- * working unchanged. New code should import from
- * `./instructions-template` directly.
- *
- * @deprecated Import from `./instructions-template` instead.
- */
-
-export {
-  CODEGRAPH_SECTION_START,
-  CODEGRAPH_SECTION_END,
-  CLAUDE_MD_TEMPLATE,
-  INSTRUCTIONS_TEMPLATE,
-} from './instructions-template';

+ 7 - 26
src/installer/config-writer.ts

@@ -11,13 +11,11 @@
  *   abstraction instead.
  */
 
-import * as fs from 'fs';
 import * as path from 'path';
 import * as os from 'os';
 import {
   writeMcpEntry,
   writePermissionsEntry,
-  writeInstructionsEntry,
 } from './targets/claude';
 import { readJsonFile } from './targets/shared';
 
@@ -25,9 +23,13 @@ export type InstallLocation = 'global' | 'local';
 
 /**
  * Each shim calls ONLY the named per-file helper — writeMcpConfig
- * writes only the MCP JSON, writePermissions only settings.json,
- * writeClaudeMd only CLAUDE.md. The full multi-file install lives
- * in `claudeTarget.install()` which the new orchestrator uses.
+ * writes only the MCP JSON, writePermissions only settings.json. The
+ * full multi-file install lives in `claudeTarget.install()` which the
+ * new orchestrator uses.
+ *
+ * There is no `writeClaudeMd` shim anymore: codegraph stopped writing a
+ * CLAUDE.md instructions block (issue #529) now that the MCP server's
+ * `initialize` instructions are the single source of truth.
  */
 export function writeMcpConfig(location: InstallLocation): void {
   writeMcpEntry(location);
@@ -37,14 +39,6 @@ export function writePermissions(location: InstallLocation): void {
   writePermissionsEntry(location);
 }
 
-export function writeClaudeMd(location: InstallLocation): { created: boolean; updated: boolean } {
-  const file = writeInstructionsEntry(location);
-  return {
-    created: file.action === 'created',
-    updated: file.action === 'updated',
-  };
-}
-
 export function hasMcpConfig(location: InstallLocation): boolean {
   // local scope lives in ./.mcp.json (project scope); global is the
   // user-scope ~/.claude.json. Mirrors the Claude target's paths.
@@ -64,16 +58,3 @@ export function hasPermissions(location: InstallLocation): boolean {
   if (!Array.isArray(allow)) return false;
   return allow.some((p: string) => p.startsWith('mcp__codegraph__'));
 }
-
-export function hasClaudeMdSection(location: InstallLocation): boolean {
-  const file = location === 'global'
-    ? path.join(os.homedir(), '.claude', 'CLAUDE.md')
-    : path.join(process.cwd(), '.claude', 'CLAUDE.md');
-  try {
-    if (!fs.existsSync(file)) return false;
-    const content = fs.readFileSync(file, 'utf-8');
-    return content.includes('<!-- CODEGRAPH_START -->') || content.includes('## CodeGraph');
-  } catch {
-    return false;
-  }
-}

+ 4 - 36
src/installer/index.ts

@@ -21,7 +21,7 @@ import {
   getTarget,
   resolveTargetFlag,
 } from './targets/registry';
-import type { AgentTarget, Location, TargetId, WriteResult } from './targets/types';
+import type { AgentTarget, Location, TargetId } from './targets/types';
 import { getGlyphs } from '../ui/glyphs';
 // Import the lightweight submodules directly (not the ../sync barrel, which
 // re-exports FileWatcher and would transitively pull in ../extraction — the
@@ -35,10 +35,8 @@ import { isGitRepo, isSyncHookInstalled, installGitSyncHook } from '../sync/git-
 export {
   writeMcpConfig,
   writePermissions,
-  writeClaudeMd,
   hasMcpConfig,
   hasPermissions,
-  hasClaudeMdSection,
 } from './config-writer';
 export type { InstallLocation } from './config-writer';
 
@@ -194,7 +192,9 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
     for (const file of result.files) {
       const verb = file.action === 'unchanged'
         ? 'Unchanged'
-        : file.action === 'created' ? 'Created' : 'Updated';
+        : file.action === 'created' ? 'Created'
+          : file.action === 'removed' ? 'Removed'
+            : 'Updated';
       clack.log.success(`${target.displayName}: ${verb} ${tildify(file.path)}`);
     }
     for (const note of result.notes ?? []) {
@@ -378,38 +378,6 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise<void>
   }
 }
 
-/**
- * For every target that has a global config and exposes
- * `wireProjectSurfaces`, write its project-local surfaces (e.g.
- * Cursor's `.cursor/rules/codegraph.mdc`). Idempotent — runs
- * silently when there's nothing to write.
- *
- * Called by `codegraph init` so that a user who ran
- * `codegraph install` once globally doesn't have to re-run it per
- * project to get full agent support.
- *
- * Returns the list of `(target, file)` pairs that were created or
- * updated — caller decides how to surface them.
- */
-export function wireProjectSurfacesForGlobalAgents(): Array<{
-  target: AgentTarget;
-  file: WriteResult['files'][number];
-}> {
-  const written: Array<{ target: AgentTarget; file: WriteResult['files'][number] }> = [];
-  for (const target of ALL_TARGETS) {
-    if (typeof target.wireProjectSurfaces !== 'function') continue;
-    const detection = target.detect('global');
-    if (!detection.alreadyConfigured) continue;
-    const result = target.wireProjectSurfaces();
-    for (const file of result.files) {
-      if (file.action === 'created' || file.action === 'updated') {
-        written.push({ target, file });
-      }
-    }
-  }
-  return written;
-}
-
 /**
  * Replace home-directory prefix in a path with `~/` for cleaner log
  * lines. Pure cosmetic.

+ 11 - 57
src/installer/instructions-template.ts

@@ -1,64 +1,18 @@
 /**
- * Agent-instructions template — the markdown body each agent target
- * writes into its conventional instructions file (CLAUDE.md /
- * AGENTS.md / codegraph.mdc / etc.).
+ * Marker constants for the legacy agent-instructions block.
  *
- * The body content is identical across agents because the codegraph
- * usage advice is agent-agnostic — only the destination filename and
- * any optional frontmatter (Cursor `.mdc`) varies per target.
+ * Codegraph used to write a `## CodeGraph` usage guide into each
+ * agent's instructions file (CLAUDE.md / AGENTS.md / GEMINI.md /
+ * codegraph.mdc / Kiro steering doc). That duplicated the guidance the
+ * MCP server already emits in its `initialize` response — every agent
+ * read the same playbook twice each turn (issue #529). The installer no
+ * longer writes an instructions file; the MCP server instructions in
+ * `mcp/server-instructions.ts` are the single source of truth.
  *
- * The legacy `claude-md-template.ts` re-exports these names for
- * backwards compatibility with downstream importers.
+ * These markers are retained so install (self-heal on upgrade) and
+ * uninstall can find and strip the block a previous install wrote.
  */
 
-/** Markers used by the marker-based section replacement. */
+/** Markers used by the marker-based section removal. */
 export const CODEGRAPH_SECTION_START = '<!-- CODEGRAPH_START -->';
 export const CODEGRAPH_SECTION_END = '<!-- CODEGRAPH_END -->';
-
-/**
- * The full marker-delimited block written into each agent's
- * instructions file. Includes the start/end markers so the section
- * can be detected and replaced on re-install.
- */
-export const INSTRUCTIONS_TEMPLATE = `${CODEGRAPH_SECTION_START}
-## CodeGraph
-
-This project has a CodeGraph MCP server (\`codegraph_*\` tools) configured. CodeGraph is a tree-sitter-parsed knowledge graph of every symbol, edge, and file. Reads are sub-millisecond and return structural information grep cannot.
-
-### When to prefer codegraph over native search
-
-Use codegraph for **structural** questions — what calls what, what would break, where is X defined, what is X's signature. Use native grep/read only for **literal text** queries (string contents, comments, log messages) or after you already have a specific file open.
-
-| Question | Tool |
-|---|---|
-| "Where is X defined?" / "Find symbol named X" | \`codegraph_search\` |
-| "What calls function Y?" | \`codegraph_callers\` |
-| "What does Y call?" | \`codegraph_callees\` |
-| "How does X reach/become Y? / trace the flow from X to Y" | \`codegraph_trace\` (one call = the whole path, incl. callback/React/JSX dynamic hops) |
-| "What would break if I changed Z?" | \`codegraph_impact\` |
-| "Show me Y's signature / source / docstring" | \`codegraph_node\` |
-| "Give me focused context for a task/area" | \`codegraph_context\` |
-| "See several related symbols' source at once" | \`codegraph_explore\` |
-| "What files exist under path/" | \`codegraph_files\` |
-| "Is the index healthy?" | \`codegraph_status\` |
-
-### Rules of thumb
-
-- **Answer directly — don't delegate exploration.** For "how does X work" / architecture questions, answer with 2-3 codegraph calls: \`codegraph_context\` first, then ONE \`codegraph_explore\` for the source of the symbols it surfaces. For a specific **flow** ("how does X reach Y") start with \`codegraph_trace\` from→to — one call returns the whole path with dynamic hops bridged — then ONE \`codegraph_explore\` for the bodies; don't rebuild the path with \`codegraph_search\` + \`codegraph_callers\`. Codegraph IS the pre-built index, so spawning a separate file-reading sub-task/agent — or running a grep + read loop — repeats work codegraph already did and costs more for the same answer.
-- **Trust codegraph results.** They come from a full AST parse. Do NOT re-verify them with grep — that's slower, less accurate, and wastes context.
-- **Don't grep first** when looking up a symbol by name. \`codegraph_search\` is faster and returns kind + location + signature in one call.
-- **Don't chain \`codegraph_search\` + \`codegraph_node\`** when you just want context — \`codegraph_context\` is one call.
-- **Don't loop \`codegraph_node\` over many symbols** — one \`codegraph_explore\` call returns several symbols' source grouped in a single capped call, while each separate node/Read call re-reads the whole context and costs far more.
-- **Index lag — check the staleness banner, don't guess a wait.** When a codegraph response starts with "⚠️ Some files referenced below were edited since the last index sync…", the listed files are pending re-index — Read those specific files for accurate content. Files NOT in that banner are fresh and codegraph is authoritative for them. \`codegraph_status\` also lists pending files under "Pending sync".
-
-### If \`.codegraph/\` doesn't exist
-
-The MCP server returns "not initialized." Ask the user: *"I notice this project doesn't have CodeGraph initialized. Want me to run \`codegraph init -i\` to build the index?"*
-${CODEGRAPH_SECTION_END}`;
-
-/**
- * Backwards-compat alias. Existing downstream code may import
- * `CLAUDE_MD_TEMPLATE` from this module via the re-export shim in
- * `claude-md-template.ts`.
- */
-export const CLAUDE_MD_TEMPLATE = INSTRUCTIONS_TEMPLATE;

+ 23 - 50
src/installer/targets/claude.ts

@@ -28,19 +28,16 @@ import {
   WriteResult,
 } from './types';
 import {
-  atomicWriteFileSync,
   getCodeGraphPermissions,
   getMcpServerConfig,
   jsonDeepEqual,
   readJsonFile,
   removeMarkedSection,
-  replaceOrAppendMarkedSection,
   writeJsonFile,
 } from './shared';
 import {
   CODEGRAPH_SECTION_END,
   CODEGRAPH_SECTION_START,
-  INSTRUCTIONS_TEMPLATE,
 } from '../instructions-template';
 
 function configDir(loc: Location): string {
@@ -123,8 +120,15 @@ class ClaudeCodeTarget implements AgentTarget {
     const hookCleanup = cleanupLegacyHooks(loc);
     if (hookCleanup.action === 'removed') files.push(hookCleanup);
 
-    // 3. CLAUDE.md instructions
-    files.push(writeInstructionsEntry(loc));
+    // 3. CLAUDE.md instructions — no longer written. The codegraph
+    // usage guidance now ships solely in the MCP server's `initialize`
+    // response (see `mcp/server-instructions.ts`), which Claude Code
+    // surfaces in the system prompt automatically. Writing it into
+    // CLAUDE.md as well meant the agent read the same playbook twice
+    // every turn (issue #529). Strip any block a previous install left
+    // behind so an upgrade self-heals — same idiom as the hook cleanup.
+    const instrCleanup = removeInstructionsEntry(loc);
+    if (instrCleanup.action === 'removed') files.push(instrCleanup);
 
     return { files };
   }
@@ -185,10 +189,8 @@ class ClaudeCodeTarget implements AgentTarget {
     const hookCleanup = cleanupLegacyHooks(loc);
     if (hookCleanup.action === 'removed') files.push(hookCleanup);
 
-    // 3. Instructions
-    const instr = instructionsPath(loc);
-    const action = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
-    files.push({ path: instr, action });
+    // 3. Instructions — strip the legacy CodeGraph block if present.
+    files.push(removeInstructionsEntry(loc));
 
     return { files };
   }
@@ -359,48 +361,19 @@ export function writePermissionsEntry(loc: Location): WriteResult['files'][numbe
   return { path: file, action: created ? 'created' : 'updated' };
 }
 
-export function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
+/**
+ * Strip the marker-delimited CodeGraph block from CLAUDE.md if a prior
+ * install wrote one. Codegraph no longer maintains an instructions file
+ * (issue #529) — the MCP server's `initialize` instructions are the
+ * single source of truth — so both install (self-heal on upgrade) and
+ * uninstall call this. `removeMarkedSection` returns `not-found`/`kept`
+ * when there's nothing to strip; the install caller drops those from
+ * the report so a fresh install stays quiet.
+ */
+export function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
   const file = instructionsPath(loc);
-  // Ensure config dir exists (for global ~/.claude/).
-  const dir = path.dirname(file);
-  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
-
-  // Honor the legacy "unmarked ## CodeGraph" rewrite path that the
-  // original installer supported (some users hand-pasted a section
-  // before markers existed). Detect first and migrate inline.
-  if (fs.existsSync(file)) {
-    const content = fs.readFileSync(file, 'utf-8');
-    if (!content.includes(CODEGRAPH_SECTION_START)) {
-      const headerMatch = content.match(/\n## CodeGraph\n/);
-      if (headerMatch && headerMatch.index !== undefined) {
-        const sectionStart = headerMatch.index;
-        const after = content.substring(sectionStart + 1);
-        const nextHeader = after.match(/\n## (?!#)/);
-        const sectionEnd = nextHeader && nextHeader.index !== undefined
-          ? sectionStart + 1 + nextHeader.index
-          : content.length;
-        const merged =
-          content.substring(0, sectionStart) +
-          '\n' + INSTRUCTIONS_TEMPLATE +
-          content.substring(sectionEnd);
-        atomicWriteFileSync(file, merged);
-        return { path: file, action: 'updated' };
-      }
-    }
-  }
-
-  const action = replaceOrAppendMarkedSection(
-    file,
-    INSTRUCTIONS_TEMPLATE,
-    CODEGRAPH_SECTION_START,
-    CODEGRAPH_SECTION_END,
-  );
-  // Map the four-state action to WriteResult's action vocabulary.
-  const mapped: 'created' | 'updated' | 'unchanged' =
-    action === 'created' ? 'created'
-      : action === 'unchanged' ? 'unchanged'
-        : 'updated';
-  return { path: file, action: mapped };
+  const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
+  return { path: file, action };
 }
 
 export const claudeTarget: AgentTarget = new ClaudeCodeTarget();

+ 15 - 21
src/installer/targets/codex.ts

@@ -28,12 +28,10 @@ import {
   atomicWriteFileSync,
   getMcpServerConfig,
   removeMarkedSection,
-  replaceOrAppendMarkedSection,
 } from './shared';
 import {
   CODEGRAPH_SECTION_END,
   CODEGRAPH_SECTION_START,
-  INSTRUCTIONS_TEMPLATE,
 } from '../instructions-template';
 import { buildTomlTable, removeTomlTable, upsertTomlTable } from './toml';
 
@@ -84,7 +82,12 @@ class CodexTarget implements AgentTarget {
     const files: WriteResult['files'] = [];
 
     files.push(writeMcpEntry());
-    files.push(writeInstructionsEntry());
+
+    // AGENTS.md is no longer written — the codegraph usage guidance
+    // ships in the MCP server's `initialize` response (issue #529).
+    // Strip a block a previous install left so an upgrade self-heals.
+    const instrCleanup = removeInstructionsEntry();
+    if (instrCleanup.action === 'removed') files.push(instrCleanup);
 
     return { files };
   }
@@ -111,9 +114,7 @@ class CodexTarget implements AgentTarget {
       files.push({ path: tomlPath, action: 'not-found' });
     }
 
-    const instr = instructionsPath();
-    const instrAction = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
-    files.push({ path: instr, action: instrAction });
+    files.push(removeInstructionsEntry());
 
     return { files };
   }
@@ -160,22 +161,15 @@ function writeMcpEntry(): WriteResult['files'][number] {
   return { path: file, action: created ? 'created' : 'updated' };
 }
 
-function writeInstructionsEntry(): WriteResult['files'][number] {
+/**
+ * Strip the marker-delimited CodeGraph block from `~/.codex/AGENTS.md`
+ * if a prior install wrote one. Used by both install (self-heal on
+ * upgrade) and uninstall — see issue #529.
+ */
+function removeInstructionsEntry(): WriteResult['files'][number] {
   const file = instructionsPath();
-  const dir = path.dirname(file);
-  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
-
-  const action = replaceOrAppendMarkedSection(
-    file,
-    INSTRUCTIONS_TEMPLATE,
-    CODEGRAPH_SECTION_START,
-    CODEGRAPH_SECTION_END,
-  );
-  const mapped: 'created' | 'updated' | 'unchanged' =
-    action === 'created' ? 'created'
-      : action === 'unchanged' ? 'unchanged'
-        : 'updated';
-  return { path: file, action: mapped };
+  const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
+  return { path: file, action };
 }
 
 export const codexTarget: AgentTarget = new CodexTarget();

+ 8 - 51
src/installer/targets/cursor.ts

@@ -46,13 +46,11 @@ import {
   getMcpServerConfig,
   jsonDeepEqual,
   readJsonFile,
-  replaceOrAppendMarkedSection,
   writeJsonFile,
 } from './shared';
 import {
   CODEGRAPH_SECTION_END,
   CODEGRAPH_SECTION_START,
-  INSTRUCTIONS_TEMPLATE,
 } from '../instructions-template';
 
 function mcpJsonPath(loc: Location): string {
@@ -112,8 +110,13 @@ class CursorTarget implements AgentTarget {
 
     files.push(writeMcpEntry(loc));
 
+    // We no longer write `.cursor/rules/codegraph.mdc` — the codegraph
+    // usage guidance ships in the MCP server's `initialize` response,
+    // the single source of truth (issue #529). Strip a rules file a
+    // previous install created so an upgrade self-heals.
     if (loc === 'local') {
-      files.push(writeRulesEntry());
+      const rulesCleanup = removeRulesEntry();
+      if (rulesCleanup.action === 'removed') files.push(rulesCleanup);
     }
 
     return {
@@ -156,16 +159,6 @@ class CursorTarget implements AgentTarget {
       ? [mcpJsonPath(loc), rulesPath()]
       : [mcpJsonPath(loc)];
   }
-
-  /**
-   * Write the project-local `.cursor/rules/codegraph.mdc` file. Used
-   * by `codegraph init` to bootstrap projects that have only the
-   * global `~/.cursor/mcp.json` — without the rules file, the Cursor
-   * agent has no signal to prefer codegraph over native grep.
-   */
-  wireProjectSurfaces(): WriteResult {
-    return { files: [writeRulesEntry()] };
-  }
 }
 
 /**
@@ -197,45 +190,9 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
   return { path: file, action };
 }
 
-function writeRulesEntry(): WriteResult['files'][number] {
-  const file = rulesPath();
-  const dir = path.dirname(file);
-  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
-
-  // Body is frontmatter + the shared instructions block. The
-  // marker-based replacement targets only the marker block, so the
-  // frontmatter is preserved across re-runs.
-  const body = MDC_FRONTMATTER + INSTRUCTIONS_TEMPLATE;
-
-  if (!fs.existsSync(file)) {
-    atomicWriteFileSync(file, body + '\n');
-    return { path: file, action: 'created' };
-  }
-
-  // For .mdc files we own outright, do byte-equality first.
-  const existing = fs.readFileSync(file, 'utf-8');
-  const wantWithNL = body + '\n';
-  if (existing === wantWithNL) {
-    return { path: file, action: 'unchanged' };
-  }
-
-  // Otherwise, marker-based section swap (preserves any user-added
-  // content outside the markers).
-  const action = replaceOrAppendMarkedSection(
-    file,
-    INSTRUCTIONS_TEMPLATE,
-    CODEGRAPH_SECTION_START,
-    CODEGRAPH_SECTION_END,
-  );
-  const mapped: 'created' | 'updated' | 'unchanged' =
-    action === 'created' ? 'created'
-      : action === 'unchanged' ? 'unchanged'
-        : 'updated';
-  return { path: file, action: mapped };
-}
-
 /**
- * Remove the Cursor rules file on uninstall.
+ * Remove the Cursor rules file on uninstall (and as a self-heal on
+ * install — see issue #529).
  *
  * Unlike the shared CLAUDE.md / AGENTS.md files (where codegraph owns
  * only a marker-delimited section), `.cursor/rules/codegraph.mdc` is a

+ 16 - 21
src/installer/targets/gemini.ts

@@ -37,13 +37,11 @@ import {
   jsonDeepEqual,
   readJsonFile,
   removeMarkedSection,
-  replaceOrAppendMarkedSection,
   writeJsonFile,
 } from './shared';
 import {
   CODEGRAPH_SECTION_END,
   CODEGRAPH_SECTION_START,
-  INSTRUCTIONS_TEMPLATE,
 } from '../instructions-template';
 
 function configDir(loc: Location): string {
@@ -85,7 +83,13 @@ class GeminiTarget implements AgentTarget {
   install(loc: Location, _opts: InstallOptions): WriteResult {
     const files: WriteResult['files'] = [];
     files.push(writeMcpEntry(loc));
-    files.push(writeInstructionsEntry(loc));
+
+    // GEMINI.md is no longer written — the codegraph usage guidance
+    // ships in the MCP server's `initialize` response (issue #529).
+    // Strip a block a previous install left so an upgrade self-heals.
+    const instrCleanup = removeInstructionsEntry(loc);
+    if (instrCleanup.action === 'removed') files.push(instrCleanup);
+
     return { files };
   }
 
@@ -108,9 +112,7 @@ class GeminiTarget implements AgentTarget {
       files.push({ path: file, action: 'not-found' });
     }
 
-    const instr = instructionsPath(loc);
-    const action = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
-    files.push({ path: instr, action });
+    files.push(removeInstructionsEntry(loc));
 
     return { files };
   }
@@ -146,22 +148,15 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
   return { path: file, action };
 }
 
-function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
+/**
+ * Strip the marker-delimited CodeGraph block from GEMINI.md if a prior
+ * install wrote one. Used by both install (self-heal on upgrade) and
+ * uninstall — see issue #529.
+ */
+function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
   const file = instructionsPath(loc);
-  const dir = path.dirname(file);
-  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
-
-  const action = replaceOrAppendMarkedSection(
-    file,
-    INSTRUCTIONS_TEMPLATE,
-    CODEGRAPH_SECTION_START,
-    CODEGRAPH_SECTION_END,
-  );
-  const mapped: 'created' | 'updated' | 'unchanged' =
-    action === 'created' ? 'created'
-      : action === 'unchanged' ? 'unchanged'
-        : 'updated';
-  return { path: file, action: mapped };
+  const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
+  return { path: file, action };
 }
 
 export const geminiTarget: AgentTarget = new GeminiTarget();

+ 10 - 30
src/installer/targets/kiro.ts

@@ -34,13 +34,11 @@ import {
   WriteResult,
 } from './types';
 import {
-  atomicWriteFileSync,
   getMcpServerConfig,
   jsonDeepEqual,
   readJsonFile,
   writeJsonFile,
 } from './shared';
-import { INSTRUCTIONS_TEMPLATE } from '../instructions-template';
 
 function configDir(loc: Location): string {
   return loc === 'global'
@@ -76,7 +74,14 @@ class KiroTarget implements AgentTarget {
   install(loc: Location, _opts: InstallOptions): WriteResult {
     const files: WriteResult['files'] = [];
     files.push(writeMcpEntry(loc));
-    files.push(writeSteeringEntry(loc));
+
+    // The steering doc is no longer written — the codegraph usage
+    // guidance ships in the MCP server's `initialize` response (issue
+    // #529). Delete a `codegraph.md` a previous install created so an
+    // upgrade self-heals.
+    const steeringCleanup = removeSteeringEntry(loc);
+    if (steeringCleanup.action === 'removed') files.push(steeringCleanup);
+
     return {
       files,
       // The IDE-only enable-MCP step is load-bearing: Kiro IDE ships
@@ -143,37 +148,12 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
   return { path: file, action };
 }
 
-/**
- * Write the dedicated steering file. Unlike CLAUDE.md / GEMINI.md
- * (shared files where codegraph owns a marker-delimited section),
- * Kiro's steering dir loads every `*.md` as a discrete document — so
- * `codegraph.md` is ours outright. Byte-equality short-circuits
- * idempotent re-runs; mismatched content gets a clean rewrite.
- */
-function writeSteeringEntry(loc: Location): WriteResult['files'][number] {
-  const file = steeringPath(loc);
-  const dir = path.dirname(file);
-  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
-
-  const body = INSTRUCTIONS_TEMPLATE + '\n';
-
-  if (!fs.existsSync(file)) {
-    atomicWriteFileSync(file, body);
-    return { path: file, action: 'created' };
-  }
-  const existing = fs.readFileSync(file, 'utf-8');
-  if (existing === body) {
-    return { path: file, action: 'unchanged' };
-  }
-  atomicWriteFileSync(file, body);
-  return { path: file, action: 'updated' };
-}
-
 /**
  * Delete the steering file we own. If a user has hand-edited the file
  * out of recognition we still remove it — codegraph.md is a name we
  * claim, and a partial install leaving the file behind is worse than
- * a clean delete.
+ * a clean delete. Used by both install (self-heal on upgrade — see
+ * issue #529) and uninstall.
  */
 function removeSteeringEntry(loc: Location): WriteResult['files'][number] {
   const file = steeringPath(loc);

+ 16 - 21
src/installer/targets/opencode.ts

@@ -41,12 +41,10 @@ import {
   atomicWriteFileSync,
   jsonDeepEqual,
   removeMarkedSection,
-  replaceOrAppendMarkedSection,
 } from './shared';
 import {
   CODEGRAPH_SECTION_END,
   CODEGRAPH_SECTION_START,
-  INSTRUCTIONS_TEMPLATE,
 } from '../instructions-template';
 
 function globalConfigDir(): string {
@@ -128,7 +126,13 @@ class OpencodeTarget implements AgentTarget {
   install(loc: Location, _opts: InstallOptions): WriteResult {
     const files: WriteResult['files'] = [];
     files.push(writeMcpEntry(loc));
-    files.push(writeInstructionsEntry(loc));
+
+    // AGENTS.md is no longer written — the codegraph usage guidance
+    // ships in the MCP server's `initialize` response (issue #529).
+    // Strip a block a previous install left so an upgrade self-heals.
+    const instrCleanup = removeInstructionsEntry(loc);
+    if (instrCleanup.action === 'removed') files.push(instrCleanup);
+
     return { files };
   }
 
@@ -163,9 +167,7 @@ class OpencodeTarget implements AgentTarget {
       }
     }
 
-    const instr = instructionsPath(loc);
-    const instrAction = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
-    files.push({ path: instr, action: instrAction });
+    files.push(removeInstructionsEntry(loc));
 
     return { files };
   }
@@ -223,22 +225,15 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
   return { path: file, action: existed ? 'updated' : 'created' };
 }
 
-function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
+/**
+ * Strip the marker-delimited CodeGraph block from AGENTS.md if a prior
+ * install wrote one. Used by both install (self-heal on upgrade) and
+ * uninstall — see issue #529.
+ */
+function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
   const file = instructionsPath(loc);
-  const dir = path.dirname(file);
-  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
-
-  const action = replaceOrAppendMarkedSection(
-    file,
-    INSTRUCTIONS_TEMPLATE,
-    CODEGRAPH_SECTION_START,
-    CODEGRAPH_SECTION_END,
-  );
-  const mapped: 'created' | 'updated' | 'unchanged' =
-    action === 'created' ? 'created'
-      : action === 'unchanged' ? 'unchanged'
-        : 'updated';
-  return { path: file, action: mapped };
+  const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
+  return { path: file, action };
 }
 
 export const opencodeTarget: AgentTarget = new OpencodeTarget();

+ 0 - 15
src/installer/targets/types.ts

@@ -103,19 +103,4 @@ export interface AgentTarget {
   printConfig(loc: Location): string;
   /** Filesystem paths this target would write to at this location. */
   describePaths(loc: Location): string[];
-  /**
-   * Optional. Write any project-local surfaces this target needs in
-   * order to work fully when its MCP config is configured globally.
-   * Called by `codegraph init` to bootstrap new projects without
-   * forcing the user to re-run `codegraph install` per project.
-   *
-   * Most targets need nothing here — their global config is complete.
-   * Cursor is the notable exception: its rules system
-   * (`.cursor/rules/*.mdc`) is project-scoped only, and is what makes
-   * Cursor's agent prefer codegraph over its built-in grep.
-   *
-   * Must be idempotent. Targets that have nothing project-local omit
-   * the method entirely.
-   */
-  wireProjectSurfaces?(): WriteResult;
 }

+ 2 - 0
src/mcp/server-instructions.ts

@@ -56,6 +56,7 @@ of calls; a grep/read exploration is dozens.
 
 ## Anti-patterns
 
+- **Trust codegraph's results — don't re-verify them with grep.** They come from a full AST parse; re-checking with grep is slower, less accurate, and wastes context.
 - **Don't grep first** when looking up a symbol by name — \`codegraph_search\` is faster and returns kind + location + signature.
 - **Don't chain \`codegraph_search\` + \`codegraph_node\`** when you just want context — \`codegraph_context\` is one round-trip.
 - **Don't loop \`codegraph_node\` over many symbols** — one \`codegraph_explore\` call returns them all grouped by file, while each separate call re-reads the whole context and costs far more. Use \`codegraph_node\` for a single symbol.
@@ -63,6 +64,7 @@ of calls; a grep/read exploration is dozens.
 
 ## Limitations
 
+- If a tool reports the project isn't initialized, \`.codegraph/\` doesn't exist yet — offer to run \`codegraph init -i\` to build the index.
 - Index lags file writes by ~1 second.
 - Cross-file resolution is best-effort name matching; ambiguous calls may return multiple candidates.
 - No live correctness validation — that's still the TypeScript compiler / test suite / linter's job. Codegraph supplements those with structural context they don't have.