diff --git a/README.md b/README.md
index 354af2463..7a73afa40 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Already installed? Run `codegraph upgrade` to update in place.
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 OpenClaw with Semantic Code Intelligence
**~16% cheaper · ~58% fewer tool calls · 100% local**
@@ -27,6 +27,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
[](#supported-agents)
[](#supported-agents)
[](#supported-agents)
+[](#supported-agents)
[](#supported-agents)
[](#supported-agents)
[](#supported-agents)
@@ -76,7 +77,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. (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 OpenClaw — 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. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)
### 3. Initialize each project
@@ -336,7 +337,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**, **OpenClaw**
- 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` / `codegraph node` commands, since the MCP server's own guidance only reaches the main agent. Removed cleanly by `codegraph uninstall`.
@@ -362,7 +363,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 / OpenClaw) for the MCP server to load.
### 3. Initialize Projects
diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts
index 91753c39c..47d8da1f2 100644
--- a/__tests__/installer-targets.test.ts
+++ b/__tests__/installer-targets.test.ts
@@ -136,6 +136,11 @@ describe('Installer targets — contract', () => {
delete seed.mcpServers;
seed.mcp = { other: { type: 'local', command: ['x'], enabled: true } };
}
+ // openclaw uses `mcp.servers.` (nested).
+ if (target.id === 'openclaw') {
+ delete seed.mcpServers;
+ seed.mcp = { servers: { other: { command: 'x' } } };
+ }
fs.writeFileSync(jsonPath, JSON.stringify(seed, null, 2) + '\n');
target.install(location, { autoAllow: true });
@@ -144,6 +149,9 @@ describe('Installer targets — contract', () => {
if (target.id === 'opencode') {
expect(after.mcp.other).toBeDefined();
expect(after.mcp.codegraph).toBeDefined();
+ } else if (target.id === 'openclaw') {
+ expect(after.mcp.servers.other).toBeDefined();
+ expect(after.mcp.servers.codegraph).toBeDefined();
} else {
expect(after.mcpServers.other).toBeDefined();
expect(after.mcpServers.codegraph).toBeDefined();
@@ -1547,4 +1555,133 @@ describe('Installer targets — opencode XDG config path (#535)', () => {
// But configuration state is read from the REAL path only.
expect(opencode.detect('global').alreadyConfigured).toBe(false);
});
+
+ // ── OpenClaw target tests ────────────────────────────────────────
+ // The OpenClaw target writes to $HOME/.openclaw/openclaw.json under
+ // mcp.servers.codegraph, and refuses local install (OpenClaw is a
+ // single gateway, no per-project config layer).
+
+ it('openclaw: install writes mcp.servers.codegraph to the resolved config file', () => {
+ const openclaw = getTarget('openclaw')!;
+ const result = openclaw.install('global', { autoAllow: true });
+ const cfg = openclaw.describePaths('global')[0];
+ expect(result.files.some((f) => f.path === cfg)).toBe(true);
+ expect(fs.existsSync(cfg)).toBe(true);
+
+ const parsed = JSON.parse(fs.readFileSync(cfg, 'utf-8'));
+ expect(parsed.mcp.servers.codegraph).toBeDefined();
+ expect(parsed.mcp.servers.codegraph.args).toContain('serve');
+ expect(parsed.mcp.servers.codegraph.args).toContain('--mcp');
+ expect(parsed.mcp.servers.codegraph.args).toContain('--path');
+ expect(parsed.mcp.servers.codegraph.env.CODEGRAPH_TELEMETRY).toBe('0');
+ });
+
+ it('openclaw: install preserves pre-existing sibling MCP servers (chrome-mcp, etc.)', () => {
+ const openclaw = getTarget('openclaw')!;
+ const cfg = openclaw.describePaths('global')[0];
+ fs.mkdirSync(path.dirname(cfg), { recursive: true });
+ fs.writeFileSync(
+ cfg,
+ JSON.stringify(
+ {
+ mcp: {
+ servers: {
+ 'chrome-mcp': { type: 'streamableHttp', url: 'http://example/mcp' },
+ },
+ },
+ },
+ null,
+ 2,
+ ) + '\n',
+ );
+
+ openclaw.install('global', { autoAllow: true });
+
+ const after = JSON.parse(fs.readFileSync(cfg, 'utf-8'));
+ expect(after.mcp.servers['chrome-mcp']).toBeDefined();
+ expect(after.mcp.servers.codegraph).toBeDefined();
+ });
+
+ it('openclaw: install preserves other top-level config keys (channels, agents, gateway)', () => {
+ const openclaw = getTarget('openclaw')!;
+ const cfg = openclaw.describePaths('global')[0];
+ fs.mkdirSync(path.dirname(cfg), { recursive: true });
+ fs.writeFileSync(
+ cfg,
+ JSON.stringify(
+ {
+ channels: { telegram: { botToken: 'redacted' } },
+ agents: { defaults: { model: 'deepseek-v4-pro' } },
+ gateway: { bind: 'loopback' },
+ },
+ null,
+ 2,
+ ) + '\n',
+ );
+
+ openclaw.install('global', { autoAllow: true });
+
+ const after = JSON.parse(fs.readFileSync(cfg, 'utf-8'));
+ expect(after.channels.telegram.botToken).toBe('redacted');
+ expect(after.agents.defaults.model).toBe('deepseek-v4-pro');
+ expect(after.gateway.bind).toBe('loopback');
+ expect(after.mcp.servers.codegraph).toBeDefined();
+ });
+
+ it('openclaw: install is idempotent (re-run produces action=unchanged)', () => {
+ const openclaw = getTarget('openclaw')!;
+ openclaw.install('global', { autoAllow: true });
+ const second = openclaw.install('global', { autoAllow: true });
+ expect(second.files.some((f) => f.action === 'unchanged')).toBe(true);
+ expect(second.files.some((f) => f.action === 'updated')).toBe(false);
+ });
+
+ it('openclaw: uninstall strips mcp.servers.codegraph but leaves siblings intact', () => {
+ const openclaw = getTarget('openclaw')!;
+ const cfg = openclaw.describePaths('global')[0];
+ fs.mkdirSync(path.dirname(cfg), { recursive: true });
+ fs.writeFileSync(
+ cfg,
+ JSON.stringify(
+ {
+ mcp: {
+ servers: {
+ 'chrome-mcp': { type: 'streamableHttp', url: 'http://example/mcp' },
+ },
+ },
+ },
+ null,
+ 2,
+ ) + '\n',
+ );
+
+ openclaw.install('global', { autoAllow: true });
+ openclaw.uninstall('global');
+
+ const after = JSON.parse(fs.readFileSync(cfg, 'utf-8'));
+ expect(after.mcp.servers['chrome-mcp']).toBeDefined();
+ expect(after.mcp.servers.codegraph).toBeUndefined();
+ });
+
+ it('openclaw: local install returns a note and writes nothing', () => {
+ const openclaw = getTarget('openclaw')!;
+ expect(openclaw.supportsLocation('local')).toBe(false);
+ const result = openclaw.install('local', { autoAllow: true });
+ expect(result.files).toEqual([]);
+ expect(result.notes && result.notes.length > 0).toBe(true);
+ });
+
+ it('openclaw: detect before install shows alreadyConfigured=false', () => {
+ const openclaw = getTarget('openclaw')!;
+ expect(openclaw.detect('global').installed).toBe(false);
+ expect(openclaw.detect('global').alreadyConfigured).toBe(false);
+ });
+
+ it('openclaw: detect after install shows alreadyConfigured=true', () => {
+ const openclaw = getTarget('openclaw')!;
+ openclaw.install('global', { autoAllow: true });
+ const d = openclaw.detect('global');
+ expect(d.installed).toBe(true);
+ expect(d.alreadyConfigured).toBe(true);
+ });
});
diff --git a/src/installer/targets/openclaw.ts b/src/installer/targets/openclaw.ts
new file mode 100644
index 000000000..d1bcbca0a
--- /dev/null
+++ b/src/installer/targets/openclaw.ts
@@ -0,0 +1,246 @@
+/**
+ * OpenClaw target.
+ *
+ * - MCP server entry merged into `~/.openclaw/openclaw.json` under
+ * `mcp.servers.codegraph`. The orchestrator then loads it on next
+ * gateway restart; the same MCP surface is exposed to all agents
+ * (main + sub-agents).
+ * - No instructions file: OpenClaw's per-agent SOUL.md / AGENTS.md
+ * pattern is user-managed. The MCP server's own `initialize`
+ * response carries the tool guidance (see `server-instructions.ts`).
+ * - No permissions concept: OpenClaw does not gate tool calls beyond
+ * the channel's per-account allowlist.
+ *
+ * The `mcp.servers` shape is OpenClaw-native:
+ * {
+ * "mcp": {
+ * "servers": {
+ * "codegraph": {
+ * "command": "",
+ * "args": ["serve", "--mcp", "--path", ""],
+ * "env": { "CODEGRAPH_TELEMETRY": "0" },
+ * "cwd": ""
+ * }
+ * }
+ * }
+ * }
+ *
+ * OpenClaw supports both stdio (this shape) and streamable-http
+ * (`{ "type": "streamableHttp", "url": "..." }`) — we use stdio.
+ *
+ * Idempotency:
+ * - If a codegraph entry already exists with the same effective
+ * command/args/env/cwd, we leave the file alone (action: unchanged).
+ * - If it exists with different fields, we update it surgically and
+ * report (action: updated).
+ * - On uninstall we drop ONLY the `mcp.servers.codegraph` key and
+ * preserve all sibling servers (chrome-mcp, wordpress-deltabis,
+ * user-defined, etc.).
+ *
+ * `--path` is REQUIRED when the MCP server's CWD is not an indexed
+ * project — without it, codegraph's `initialize` response says
+ * "workspace not indexed" and exposes no tools. The installer
+ * auto-detects the project root with the same heuristic as `codegraph
+ * init`: walk up from cwd looking for a `.codegraph/` directory, falling
+ * back to cwd.
+ */
+
+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, jsonDeepEqual } from './shared';
+
+/**
+ * OpenClaw is a single-user gateway with one global config dir
+ * (XDG-style, like opencode). Local install is not supported — there
+ * is no per-project config layer to write into.
+ */
+function openclawConfigDir(): string {
+ const xdg = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim().length > 0
+ ? process.env.XDG_CONFIG_HOME
+ : path.join(os.homedir(), '.config');
+ // OpenClaw keeps its own dir at $XDG_CONFIG_HOME/openclaw OR $HOME/.openclaw;
+ // the latter is the documented default and what 99% of users have.
+ return fs.existsSync(path.join(os.homedir(), '.openclaw'))
+ ? path.join(os.homedir(), '.openclaw')
+ : path.join(xdg, 'openclaw');
+}
+
+function configPath(): string {
+ return path.join(openclawConfigDir(), 'openclaw.json');
+}
+
+function readConfigText(file: string): string {
+ if (!fs.existsSync(file)) return '';
+ return fs.readFileSync(file, 'utf-8');
+}
+
+function parseConfig(text: string): Record {
+ if (!text.trim()) return {};
+ try {
+ const result = JSON.parse(text);
+ if (result == null || typeof result !== 'object' || Array.isArray(result)) {
+ return {};
+ }
+ return result as Record;
+ } catch {
+ return {};
+ }
+}
+
+/**
+ * Find the project root by walking up from cwd looking for an existing
+ * `.codegraph/` directory. Falls back to cwd if none found. This
+ * mirrors what `codegraph init` does for the auto-detected case.
+ */
+function detectProjectRoot(): string {
+ let dir = process.cwd();
+ for (let i = 0; i < 10; i++) {
+ if (fs.existsSync(path.join(dir, '.codegraph'))) return dir;
+ const parent = path.dirname(dir);
+ if (parent === dir) break;
+ dir = parent;
+ }
+ return process.cwd();
+}
+
+/**
+ * Resolve the codegraph binary path. Prefer the install location
+ * populated by `install.sh` / `npm i -g`; fall back to `which codegraph`
+ * via PATH; finally fall back to `codegraph` (PATH-resolved by OpenClaw).
+ */
+function codegraphBinaryPath(): string {
+ const home = os.homedir();
+ const candidates = [
+ path.join(home, '.local', 'bin', 'codegraph'),
+ path.join(home, '.codegraph', 'bin', 'codegraph'),
+ '/usr/local/bin/codegraph',
+ '/opt/homebrew/bin/codegraph',
+ ];
+ for (const c of candidates) {
+ if (fs.existsSync(c)) return c;
+ }
+ return 'codegraph'; // PATH-resolved fallback
+}
+
+function buildServerEntry(): Record {
+ const project = detectProjectRoot();
+ return {
+ command: codegraphBinaryPath(),
+ args: ['serve', '--mcp', '--path', project],
+ env: { CODEGRAPH_TELEMETRY: '0' },
+ cwd: project,
+ };
+}
+
+class OpenclawTarget implements AgentTarget {
+ readonly id = 'openclaw' as const;
+ readonly displayName = 'OpenClaw';
+ readonly docsUrl = 'https://github.com/openclaw/openclaw';
+
+ supportsLocation(_loc: Location): boolean {
+ // OpenClaw is global-only; it has no per-project config layer.
+ return _loc === 'global';
+ }
+
+ detect(loc: Location): DetectionResult {
+ if (loc !== 'global') return { installed: false, alreadyConfigured: false };
+ const file = configPath();
+ const dir = openclawConfigDir();
+ const installed = fs.existsSync(dir) && fs.existsSync(file);
+ const config = parseConfig(readConfigText(file));
+ const alreadyConfigured = !!config.mcp?.servers?.codegraph;
+ return { installed, alreadyConfigured, configPath: file };
+ }
+
+ install(loc: Location, _opts: InstallOptions): WriteResult {
+ if (loc !== 'global') {
+ return {
+ files: [],
+ notes: ['OpenClaw only supports global install (single gateway, no per-project config).'],
+ };
+ }
+ return { files: [writeMcpEntry()] };
+ }
+
+ uninstall(loc: Location): WriteResult {
+ if (loc !== 'global') return { files: [] };
+ return { files: [removeMcpEntry()] };
+ }
+
+ printConfig(loc: Location): string {
+ if (loc !== 'global') {
+ return '# OpenClaw only supports global install.\n';
+ }
+ const target = configPath();
+ const snippet = JSON.stringify(
+ { mcp: { servers: { codegraph: buildServerEntry() } } },
+ null,
+ 2,
+ );
+ return `# Add to ${target}\n\n${snippet}\n`;
+ }
+
+ describePaths(loc: Location): string[] {
+ return loc === 'global' ? [configPath()] : [];
+ }
+}
+
+function writeMcpEntry(): WriteResult['files'][number] {
+ const file = configPath();
+ const dir = path.dirname(file);
+ const existed = fs.existsSync(file);
+
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+
+ const text = readConfigText(file);
+ const config = parseConfig(text);
+ const before = config.mcp?.servers?.codegraph;
+ const after = buildServerEntry();
+
+ if (jsonDeepEqual(before, after)) {
+ return { path: file, action: 'unchanged' };
+ }
+
+ // Initialize nested mcp.servers.codegraph structure if absent
+ if (!config.mcp || typeof config.mcp !== 'object') {
+ config.mcp = {};
+ }
+ if (!config.mcp.servers || typeof config.mcp.servers !== 'object') {
+ config.mcp.servers = {};
+ }
+ config.mcp.servers.codegraph = after;
+
+ atomicWriteFileSync(file, JSON.stringify(config, null, 2) + '\n');
+ return { path: file, action: existed ? 'updated' : 'created' };
+}
+
+/**
+ * Drop ONLY `mcp.servers.codegraph`; preserve every other server,
+ * channel, agent, etc. If `mcp.servers` is empty afterwards, leave
+ * the wrapper (OpenClaw schema tolerates an empty servers object).
+ */
+function removeMcpEntry(): WriteResult['files'][number] {
+ const file = configPath();
+ if (!fs.existsSync(file)) return { path: file, action: 'not-found' };
+ const text = readConfigText(file);
+ const config = parseConfig(text);
+ if (!config.mcp?.servers?.codegraph) {
+ return { path: file, action: 'not-found' };
+ }
+
+ delete config.mcp.servers.codegraph;
+ atomicWriteFileSync(file, JSON.stringify(config, null, 2) + '\n');
+ return { path: file, action: 'removed' };
+}
+
+export const openclawTarget: AgentTarget = new OpenclawTarget();
diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts
index 5e929d468..2236d497e 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 { openclawTarget } from './openclaw';
export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
claudeTarget,
@@ -26,6 +27,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
geminiTarget,
antigravityTarget,
kiroTarget,
+ openclawTarget,
]);
export function getTarget(id: string): AgentTarget | undefined {
diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts
index 4b3267e97..ce4d9280b 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' | 'openclaw';
/**
* Result of `target.detect(location)`.