diff --git a/README.md b/README.md
index dbaa2ad69..3c64c607f 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Already installed? Run `codegraph upgrade`
Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
-### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, and Kiro with Semantic Code Intelligence
+### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, Kiro, and Grok Build with Semantic Code Intelligence
**Surgical context · fewer tool calls · faster answers · 100% local**
@@ -32,6 +32,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
[](#supported-agents)
[](#supported-agents)
[](#supported-agents)
+[](#supported-agents)
@@ -102,7 +103,7 @@ In a **new terminal**, run the installer to connect CodeGraph to the agents you
codegraph install
```
-Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. It only wires up your agent — it does **not** index any code; building each project's graph is the separate `codegraph init` in step 3. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)
+Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and Grok Build — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. It only wires up your agent — it does **not** index any code; building each project's graph is the separate `codegraph init` in step 3. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)
### 3. Initialize each project
@@ -415,7 +416,7 @@ npx @colbymchenry/codegraph
```
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**
+- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**, **Grok Build**
- 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, plus a small marker-fenced CodeGraph section in the agent's instructions file (`CLAUDE.md` / `AGENTS.md` / `GEMINI.md`) — that's how subagents and non-MCP agents learn the `codegraph explore` command, since the MCP server's own guidance only reaches the main agent. Removed cleanly by `codegraph uninstall`.
@@ -442,7 +443,7 @@ codegraph install --print-config codex # print snippet, no file wr
### 2. Restart Your Agent
-Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro) for the MCP server to load.
+Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro / Grok Build) for the MCP server to load.
### 3. Initialize Projects
@@ -800,6 +801,7 @@ is written):
- **Gemini CLI**
- **Antigravity IDE**
- **Kiro**
+- **Grok Build**
## Supported Languages
@@ -908,7 +910,7 @@ MIT
-**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro**
+**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and Grok Build**
[Report Bug](https://github.com/colbymchenry/codegraph/issues) · [Request Feature](https://github.com/colbymchenry/codegraph/issues)
diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts
index e11efd3cc..891b95d1d 100644
--- a/__tests__/installer-targets.test.ts
+++ b/__tests__/installer-targets.test.ts
@@ -1187,6 +1187,7 @@ describe('Installer targets — registry', () => {
expect(getTarget('gemini')?.id).toBe('gemini');
expect(getTarget('antigravity')?.id).toBe('antigravity');
expect(getTarget('kiro')?.id).toBe('kiro');
+ expect(getTarget('grok')?.id).toBe('grok');
expect(getTarget('not-a-real-target')).toBeUndefined();
});
@@ -1571,6 +1572,214 @@ function listAllFiles(dir: string): string[] {
// SAME directory (which is exactly how this bug stayed invisible), so these
// tests deliberately split them.
// ---------------------------------------------------------------------------
+describe('Installer targets — Grok Build', () => {
+ let tmpHome: string;
+ let tmpCwd: string;
+ let origCwd: string;
+ let homeRestore: { restore: () => void };
+
+ beforeEach(() => {
+ tmpHome = mkTmpDir('home');
+ tmpCwd = mkTmpDir('cwd');
+ origCwd = process.cwd();
+ process.chdir(tmpCwd);
+ homeRestore = setHome(tmpHome);
+ });
+
+ afterEach(() => {
+ homeRestore.restore();
+ process.chdir(origCwd);
+ fs.rmSync(tmpHome, { recursive: true, force: true });
+ fs.rmSync(tmpCwd, { recursive: true, force: true });
+ });
+
+ it('grok: install writes [mcp_servers.codegraph] to ~/.grok/config.toml AND the AGENTS.md codegraph block', () => {
+ const grok = getTarget('grok')!;
+ const first = grok.install('global', { autoAllow: false });
+ const tomlPath = path.join(tmpHome, '.grok', 'config.toml');
+ const agentsMd = path.join(tmpHome, '.grok', 'AGENTS.md');
+ expect(first.files.some((f) => f.path.endsWith('config.toml'))).toBe(true);
+ expect(fs.existsSync(tomlPath)).toBe(true);
+ const toml = fs.readFileSync(tomlPath, 'utf-8');
+ expect(toml).toContain('[mcp_servers.codegraph]');
+ expect(toml).toContain('command = "codegraph"');
+ expect(toml).toContain('args = ["serve", "--mcp"]');
+ // The short instructions block IS written (subagents / non-MCP
+ // harnesses read AGENTS.md but never the MCP initialize instructions).
+ expect(fs.existsSync(agentsMd)).toBe(true);
+ const body = fs.readFileSync(agentsMd, 'utf-8');
+ expect(body).toContain('## CodeGraph');
+ expect(body).toContain('codegraph explore');
+ // Re-install is fully unchanged (byte-equal block → idempotent).
+ const second = grok.install('global', { autoAllow: false });
+ for (const f of second.files) expect(f.action).toBe('unchanged');
+ });
+
+ it('grok: install preserves a pre-existing sibling MCP server in config.toml', () => {
+ const grok = getTarget('grok')!;
+ const dir = path.join(tmpHome, '.grok');
+ fs.mkdirSync(dir, { recursive: true });
+ const tomlPath = path.join(dir, 'config.toml');
+ fs.writeFileSync(tomlPath, [
+ '[mcp_servers.github]',
+ 'command = "npx"',
+ 'args = ["-y", "@modelcontextprotocol/server-github"]',
+ '',
+ ].join('\n'));
+
+ grok.install('global', { autoAllow: false });
+
+ const after = fs.readFileSync(tomlPath, 'utf-8');
+ expect(after).toContain('[mcp_servers.github]');
+ expect(after).toContain('server-github');
+ expect(after).toContain('[mcp_servers.codegraph]');
+ });
+
+ it('grok: install preserves other top-level TOML sections (cli, models, features)', () => {
+ const grok = getTarget('grok')!;
+ const dir = path.join(tmpHome, '.grok');
+ fs.mkdirSync(dir, { recursive: true });
+ const tomlPath = path.join(dir, 'config.toml');
+ fs.writeFileSync(tomlPath, [
+ '[cli]',
+ 'auto_update = true',
+ '',
+ '[models]',
+ 'default = "grok-build"',
+ '',
+ ].join('\n'));
+
+ grok.install('global', { autoAllow: false });
+
+ const after = fs.readFileSync(tomlPath, 'utf-8');
+ expect(after).toContain('auto_update = true');
+ expect(after).toContain('default = "grok-build"');
+ expect(after).toContain('[mcp_servers.codegraph]');
+ });
+
+ it('grok: install replaces a legacy AGENTS.md codegraph block with the current one, keeping user content', () => {
+ const grok = getTarget('grok')!;
+ const dir = path.join(tmpHome, '.grok');
+ fs.mkdirSync(dir, { recursive: true });
+ const agentsMd = path.join(dir, 'AGENTS.md');
+ fs.writeFileSync(agentsMd, `# My grok notes\n\nBe terse.\n\n${LEGACY_BLOCK}\n`);
+
+ const result = grok.install('global', { autoAllow: false });
+
+ const body = fs.readFileSync(agentsMd, 'utf-8');
+ expect(body).toContain('# My grok notes');
+ expect(body).toContain('Be terse.');
+ // Self-heal: the stale pre-#529 body is gone, the current block is in.
+ expect(body).not.toContain('Prefer `codegraph_search`');
+ expect(body).toContain('codegraph explore');
+ const mdEntry = result.files.find((f) => f.path.endsWith('AGENTS.md'));
+ expect(mdEntry?.action).toBe('updated');
+ });
+
+ it('grok: uninstall strips [mcp_servers.codegraph] but leaves siblings intact', () => {
+ const grok = getTarget('grok')!;
+ const dir = path.join(tmpHome, '.grok');
+ fs.mkdirSync(dir, { recursive: true });
+ const tomlPath = path.join(dir, 'config.toml');
+ // Pre-seed with a sibling MCP server.
+ fs.writeFileSync(tomlPath, [
+ '[mcp_servers.github]',
+ 'command = "npx"',
+ 'args = ["-y", "@modelcontextprotocol/server-github"]',
+ '',
+ '[mcp_servers.codegraph]',
+ 'command = "codegraph"',
+ 'args = ["serve", "--mcp"]',
+ '',
+ ].join('\n'));
+
+ grok.uninstall('global');
+
+ const after = fs.readFileSync(tomlPath, 'utf-8');
+ expect(after).not.toContain('[mcp_servers.codegraph]');
+ expect(after).toContain('[mcp_servers.github]');
+ });
+
+ it('grok: local install writes ./.grok/config.toml (project scope)', () => {
+ const grok = getTarget('grok')!;
+ const result = grok.install('local', { autoAllow: false });
+ const tomlPath = path.join(tmpCwd, '.grok', 'config.toml');
+ // macOS symlinks /var → /private/var; fs.realpathSync normalizes both.
+ expect(result.files.some((f) => fs.realpathSync(f.path) === fs.realpathSync(tomlPath))).toBe(true);
+ expect(fs.existsSync(tomlPath)).toBe(true);
+ const toml = fs.readFileSync(tomlPath, 'utf-8');
+ expect(toml).toContain('[mcp_servers.codegraph]');
+ // Local install does NOT write AGENTS.md (that's global only).
+ expect(result.files.some((f) => f.path.endsWith('AGENTS.md'))).toBe(false);
+ });
+
+ it('grok: local uninstall removes config.toml when it becomes empty', () => {
+ const grok = getTarget('grok')!;
+ grok.install('local', { autoAllow: false });
+ const tomlPath = path.join(tmpCwd, '.grok', 'config.toml');
+ expect(fs.existsSync(tomlPath)).toBe(true);
+
+ grok.uninstall('local');
+
+ // The only entry was codegraph, so the file is removed entirely.
+ expect(fs.existsSync(tomlPath)).toBe(false);
+ // The .grok/ dir should also be cleaned up.
+ expect(fs.existsSync(path.join(tmpCwd, '.grok'))).toBe(false);
+ });
+
+ it('grok: local uninstall keeps config.toml when sibling servers exist', () => {
+ const grok = getTarget('grok')!;
+ grok.install('local', { autoAllow: false });
+ const tomlPath = path.join(tmpCwd, '.grok', 'config.toml');
+ // Add a sibling server.
+ const content = fs.readFileSync(tomlPath, 'utf-8');
+ const withSibling = content.replace(
+ /\n*$/,
+ '\n\n[mcp_servers.github]\ncommand = "npx"\nargs = ["-y", "@modelcontextprotocol/server-github"]\n',
+ );
+ fs.writeFileSync(tomlPath, withSibling);
+
+ grok.uninstall('local');
+
+ expect(fs.existsSync(tomlPath)).toBe(true);
+ const after = fs.readFileSync(tomlPath, 'utf-8');
+ expect(after).not.toContain('[mcp_servers.codegraph]');
+ expect(after).toContain('[mcp_servers.github]');
+ });
+
+ it('grok: detect before install shows alreadyConfigured=false', () => {
+ const grok = getTarget('grok')!;
+ expect(grok.detect('global').alreadyConfigured).toBe(false);
+ expect(grok.detect('local').alreadyConfigured).toBe(false);
+ });
+
+ it('grok: detect after install shows alreadyConfigured=true', () => {
+ const grok = getTarget('grok')!;
+ grok.install('global', { autoAllow: false });
+ expect(grok.detect('global').alreadyConfigured).toBe(true);
+ // Local was not installed, so it should still be false.
+ expect(grok.detect('local').alreadyConfigured).toBe(false);
+ });
+
+ it('grok: GROK_HOME env var overrides default config dir', () => {
+ const grok = getTarget('grok')!;
+ const customHome = mkTmpDir('grok-home');
+ process.env.GROK_HOME = customHome;
+ try {
+ grok.install('global', { autoAllow: false });
+ const tomlPath = path.join(customHome, 'config.toml');
+ expect(fs.existsSync(tomlPath)).toBe(true);
+ const toml = fs.readFileSync(tomlPath, 'utf-8');
+ expect(toml).toContain('[mcp_servers.codegraph]');
+ // Default location should NOT have been written.
+ expect(fs.existsSync(path.join(tmpHome, '.grok', 'config.toml'))).toBe(false);
+ } finally {
+ delete process.env.GROK_HOME;
+ fs.rmSync(customHome, { recursive: true, force: true });
+ }
+ });
+});
+
describe('Installer targets — opencode XDG config path (#535)', () => {
let tmpHome: string;
let tmpCwd: string;
diff --git a/src/installer/targets/grok.ts b/src/installer/targets/grok.ts
new file mode 100644
index 000000000..01256b514
--- /dev/null
+++ b/src/installer/targets/grok.ts
@@ -0,0 +1,199 @@
+/**
+ * Grok Build (xAI) target. Writes:
+ *
+ * - MCP server entry to `~/.grok/config.toml` (global) or
+ * `./.grok/config.toml` (local) as the dotted-key table
+ * `[mcp_servers.codegraph]`. TOML — not JSON — handled by
+ * the narrow serializer in `./toml.ts` (same as Codex).
+ *
+ * Grok reads project rules from `AGENTS.md` files natively, so no
+ * separate instructions file is needed — the MCP `initialize`
+ * response is the single source of truth for agent-facing guidance
+ * (issue #529). A prior install that wrote a ``
+ * block into an AGENTS.md is self-healed on upgrade (stripped by
+ * install, same as other targets).
+ *
+ * No permissions concept — Grok gates tool invocations through its
+ * own UI prompts rather than an external allowlist. `autoAllow` is
+ * silently ignored.
+ *
+ * Config dir resolution: `~/.grok/` (preferred) or `$GROK_HOME`
+ * (env override). Project-scoped config lives at `.grok/config.toml`
+ * relative to the project root.
+ *
+ * Docs: https://docs.x.ai/docs/mcp
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+import {
+ AgentTarget,
+ DetectionResult,
+ InstallOptions,
+ Location,
+ WriteResult,
+} from './types';
+import {
+ atomicWriteFileSync,
+ getMcpServerConfig,
+ removeMarkedSection,
+ upsertInstructionsEntry,
+} from './shared';
+import {
+ CODEGRAPH_SECTION_END,
+ CODEGRAPH_SECTION_START,
+} from '../instructions-template';
+import { buildTomlTable, removeTomlTable, upsertTomlTable } from './toml';
+
+const TOML_HEADER = 'mcp_servers.codegraph';
+
+function globalConfigDir(): string {
+ const grokHome = process.env.GROK_HOME;
+ return grokHome || path.join(os.homedir(), '.grok');
+}
+function tomlConfigPath(loc: Location): string {
+ return loc === 'global'
+ ? path.join(globalConfigDir(), 'config.toml')
+ : path.join(process.cwd(), '.grok', 'config.toml');
+}
+/**
+ * Grok reads AGENTS.md natively from the project root and from
+ * `~/.grok/`. We write the marker-fenced block to the global
+ * AGENTS.md so subagents and non-MCP harnesses see it (#704).
+ * Project-local AGENTS.md at the cwd is left to the user.
+ */
+function instructionsPath(): string {
+ return path.join(globalConfigDir(), 'AGENTS.md');
+}
+
+class GrokTarget implements AgentTarget {
+ readonly id = 'grok' as const;
+ readonly displayName = 'Grok Build';
+ readonly docsUrl = 'https://docs.x.ai/docs/mcp';
+
+ supportsLocation(_loc: Location): boolean {
+ return true;
+ }
+
+ detect(loc: Location): DetectionResult {
+ const tomlPath = tomlConfigPath(loc);
+ let alreadyConfigured = false;
+ if (fs.existsSync(tomlPath)) {
+ try {
+ const content = fs.readFileSync(tomlPath, 'utf-8');
+ alreadyConfigured = content.includes(`[${TOML_HEADER}]`);
+ } catch { /* ignore */ }
+ }
+ const configDir = loc === 'global' ? globalConfigDir() : path.join(process.cwd(), '.grok');
+ const installed = fs.existsSync(configDir) || fs.existsSync(tomlPath);
+ return { installed, alreadyConfigured, configPath: tomlPath };
+ }
+
+ install(loc: Location, _opts: InstallOptions): WriteResult {
+ const files: WriteResult['files'] = [];
+
+ files.push(writeMcpEntry(loc));
+
+ // Global AGENTS.md gets the short marker-fenced CodeGraph block (#704):
+ // subagents and non-MCP harnesses read AGENTS.md but never the MCP
+ // initialize instructions. Upsert self-heals a stale pre-#529 block.
+ // Only write for global — project-local AGENTS.md is the user's domain.
+ if (loc === 'global') {
+ files.push(upsertInstructionsEntry(instructionsPath()));
+ }
+
+ return {
+ files,
+ notes: ['Restart Grok for MCP changes to take effect.'],
+ };
+ }
+
+ uninstall(loc: Location): WriteResult {
+ const files: WriteResult['files'] = [];
+
+ const tomlPath = tomlConfigPath(loc);
+ if (fs.existsSync(tomlPath)) {
+ const content = fs.readFileSync(tomlPath, 'utf-8');
+ const { content: nextContent, action } = removeTomlTable(content, TOML_HEADER);
+ if (action === 'removed') {
+ if (nextContent.trim() === '') {
+ try { fs.unlinkSync(tomlPath); } catch { /* ignore */ }
+ // Also remove the .grok/ dir if it's now empty (project-local only;
+ // never remove the global ~/.grok/ — it has other files).
+ if (loc === 'local') {
+ const dir = path.dirname(tomlPath);
+ try {
+ if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
+ } catch { /* ignore */ }
+ }
+ } else {
+ atomicWriteFileSync(tomlPath, nextContent.trimEnd() + '\n');
+ }
+ files.push({ path: tomlPath, action: 'removed' });
+ } else {
+ files.push({ path: tomlPath, action: 'not-found' });
+ }
+ } else {
+ files.push({ path: tomlPath, action: 'not-found' });
+ }
+
+ // Strip the marker-delimited block from global AGENTS.md if a prior
+ // install wrote one. Only for global — local AGENTS.md is the user's.
+ if (loc === 'global') {
+ files.push(removeInstructionsEntry());
+ }
+
+ return { files };
+ }
+
+ printConfig(loc: Location): string {
+ const block = buildCodegraphBlock();
+ const target = tomlConfigPath(loc);
+ return `# Add to ${target}\n\n${block}\n`;
+ }
+
+ describePaths(loc: Location): string[] {
+ const paths = [tomlConfigPath(loc)];
+ if (loc === 'global') paths.push(instructionsPath());
+ return paths;
+ }
+}
+
+function buildCodegraphBlock(): string {
+ const mcp = getMcpServerConfig();
+ return buildTomlTable(TOML_HEADER, {
+ command: mcp.command,
+ args: mcp.args,
+ });
+}
+
+function writeMcpEntry(loc: Location): WriteResult['files'][number] {
+ const file = tomlConfigPath(loc);
+ const dir = path.dirname(file);
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
+
+ const block = buildCodegraphBlock();
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
+ const created = existing.length === 0;
+ const { content: nextContent, action } = upsertTomlTable(existing, TOML_HEADER, block);
+
+ if (action === 'unchanged') {
+ return { path: file, action: 'unchanged' };
+ }
+ atomicWriteFileSync(file, nextContent);
+ return { path: file, action: created ? 'created' : 'updated' };
+}
+
+/**
+ * Strip the marker-delimited CodeGraph block from `~/.grok/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 action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
+ return { path: file, action };
+}
+
+export const grokTarget: AgentTarget = new GrokTarget();
diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts
index 5e929d468..6346d66bc 100644
--- a/src/installer/targets/registry.ts
+++ b/src/installer/targets/registry.ts
@@ -16,6 +16,7 @@ import { hermesTarget } from './hermes';
import { geminiTarget } from './gemini';
import { antigravityTarget } from './antigravity';
import { kiroTarget } from './kiro';
+import { grokTarget } from './grok';
export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
claudeTarget,
@@ -26,6 +27,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
geminiTarget,
antigravityTarget,
kiroTarget,
+ grokTarget,
]);
export function getTarget(id: string): AgentTarget | undefined {
diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts
index 833a801ae..3e0c9d6c5 100644
--- a/src/installer/targets/types.ts
+++ b/src/installer/targets/types.ts
@@ -19,7 +19,7 @@ export type Location = 'global' | 'local';
* lookup. New targets add a value here when they're added to the
* registry. Keep these short and lowercase.
*/
-export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro';
+export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro' | 'grok';
/**
* Result of `target.detect(location)`.