diff --git a/README.md b/README.md
index 6a6462d56..3868fce6b 100644
--- a/README.md
+++ b/README.md
@@ -230,6 +230,11 @@ agentmemory works with any agent that supports hooks, MCP, or REST API. All agen
Warp
connect + MCP + skills
+
+
+Klaat Code
+6 hooks + MCP
+ |
@@ -709,6 +714,7 @@ The agentmemory entry is the **same MCP server block** across every host that us
| **Antigravity** (replaces Gemini CLI) | `mcp_config.json` (in Antigravity's User dir) | `agentmemory connect antigravity` writes the standard `mcpServers` block. macOS: `~/Library/Application Support/Antigravity/User/`. Linux: `~/.config/Antigravity/User/`. Use after the 2026-06-18 Gemini CLI sunset. |
| **Antigravity CLI** (`agy`) | `~/.gemini/config/mcp_config.json` | `agentmemory connect antigravity-cli`. The `agy` CLI keeps its own config under `~/.gemini/`, separate from the Antigravity IDE above. Pass `--with-hooks` for native auto-capture via `~/.gemini/config/hooks.json`. |
| **Kiro** | `~/.kiro/settings/mcp.json` | `agentmemory connect kiro` writes the user-level config. Workspace overrides go in `.kiro/settings/mcp.json` next to your code. |
+| **Klaat Code (MCP + hooks)** | `~/.klaatai/mcp.json` | `agentmemory connect klaatcode` merges the MCP entry under Klaat Code's `servers` key (not `mcpServers`); `--with-hooks` adds six native auto-capture hooks (`session_start`, `before_message`, `before_tool`, `after_tool`, `after_message`, `session_end`) into `~/.klaatai/hooks.json` with Klaat Code's snake_case tool matchers. Verify with `/mcp` and `/hooks` inside klaatcode. Hooks fire in the interactive TUI only — `klaatai run` (headless) and ACP sessions are not captured. Klaat Code also auto-loads `.claude/skills/`, so the 17 agentmemory skills work with no extra step. |
| **Warp** | `~/.warp/.mcp.json` | `agentmemory connect warp` writes the standard `mcpServers` block. Warp also auto-discovers skills from `.claude/skills/`; once the Claude Code plugin is installed the 8 agentmemory skills (`remember`, `recall`, `recap`, `handoff`, `forget`, `commit-context`, `commit-history`, `session-history`) appear natively in Warp's slash-command palette. |
| **Cline (CLI)** | `~/.cline/mcp.json` | `agentmemory connect cline` writes the standard `mcpServers` block. VS Code extension users: paste the same block via Cline Settings → MCP Servers → Edit JSON. |
| **Continue.dev** | `~/.continue/config.yaml` (preferred) or `config.json` (legacy) | `agentmemory connect continue` creates `config.yaml` from scratch when neither exists, or modifies existing `config.json`. **If you already have `config.yaml`** the adapter prints the exact block to paste under `mcpServers:`; it won't silently rewrite your yaml because preserving comments and anchors safely needs a YAML parser the package doesn't ship. Continue uses array form (not object) for `mcpServers`. |
diff --git a/plugin/hooks/hooks.klaatcode.json b/plugin/hooks/hooks.klaatcode.json
new file mode 100644
index 000000000..9a4bac8b0
--- /dev/null
+++ b/plugin/hooks/hooks.klaatcode.json
@@ -0,0 +1,39 @@
+{
+ "session_start": [
+ {
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\"",
+ "timeout": 5
+ }
+ ],
+ "before_message": [
+ {
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs\"",
+ "timeout": 5
+ }
+ ],
+ "before_tool": [
+ {
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\"",
+ "matcher": "^(run_command|edit_file|multi_edit|write_file|read_file|apply_patch|grep|glob)$",
+ "timeout": 5
+ }
+ ],
+ "after_tool": [
+ {
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs\"",
+ "timeout": 5
+ }
+ ],
+ "after_message": [
+ {
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs\"",
+ "timeout": 5
+ }
+ ],
+ "session_end": [
+ {
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-end.mjs\"",
+ "timeout": 5
+ }
+ ]
+}
diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs
index dcf2e3931..6e35018f4 100755
--- a/plugin/scripts/notification.mjs
+++ b/plugin/scripts/notification.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/notification.ts
diff --git a/plugin/scripts/post-commit.mjs b/plugin/scripts/post-commit.mjs
index 56a17ccea..995f11415 100755
--- a/plugin/scripts/post-commit.mjs
+++ b/plugin/scripts/post-commit.mjs
@@ -9,8 +9,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/post-commit.ts
diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs
index 0af94e7cd..0f4532f5f 100755
--- a/plugin/scripts/post-tool-failure.mjs
+++ b/plugin/scripts/post-tool-failure.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/post-tool-failure.ts
diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs
index 1189e35fe..a164d8eda 100755
--- a/plugin/scripts/post-tool-use.mjs
+++ b/plugin/scripts/post-tool-use.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/post-tool-use.ts
diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs
index 753094359..50352a5cb 100755
--- a/plugin/scripts/pre-compact.mjs
+++ b/plugin/scripts/pre-compact.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/pre-compact.ts
diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs
index 53daba26c..c1c600e40 100755
--- a/plugin/scripts/prompt-submit.mjs
+++ b/plugin/scripts/prompt-submit.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/prompt-submit.ts
diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs
index f2d8f79b1..20d845e8a 100755
--- a/plugin/scripts/session-end.mjs
+++ b/plugin/scripts/session-end.mjs
@@ -28,8 +28,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/session-end.ts
diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs
index 7c112250c..40ba01ffc 100755
--- a/plugin/scripts/session-start.mjs
+++ b/plugin/scripts/session-start.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/session-start.ts
diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs
index 722f0c7f0..82eb74d7f 100755
--- a/plugin/scripts/subagent-start.mjs
+++ b/plugin/scripts/subagent-start.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/subagent-start.ts
diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs
index 8927c1af6..2e84ecfcd 100755
--- a/plugin/scripts/subagent-stop.mjs
+++ b/plugin/scripts/subagent-stop.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/subagent-stop.ts
diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs
index 613a9cd9b..b0a9a03bd 100755
--- a/plugin/scripts/task-completed.mjs
+++ b/plugin/scripts/task-completed.mjs
@@ -27,8 +27,15 @@ function hookCwd(data) {
if (Array.isArray(roots)) {
for (const root of roots) if (typeof root === "string" && root.trim()) return root;
}
- const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root;
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT"
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
}
//#endregion
//#region src/hooks/task-completed.ts
diff --git a/plugin/skills/agentmemory-agents/REFERENCE.md b/plugin/skills/agentmemory-agents/REFERENCE.md
index 84d637d8e..32d165d66 100644
--- a/plugin/skills/agentmemory-agents/REFERENCE.md
+++ b/plugin/skills/agentmemory-agents/REFERENCE.md
@@ -3,7 +3,7 @@
Generated from `src/cli/connect/index.ts`. Do not edit the block below by hand; run `npm run skills:gen` after adding or removing an adapter.
-`agentmemory connect ` wires the memory server into a host agent. 21 adapters:
+`agentmemory connect ` wires the memory server into a host agent. 22 adapters:
| Agent | Name | Protocol |
| --- | --- | --- |
@@ -21,6 +21,7 @@ Generated from `src/cli/connect/index.ts`. Do not edit the block below by hand;
| Gemini CLI | `gemini-cli` | Using MCP (the only protocol Gemini CLI speaks). Memory bridge runs at :3111 underneath. |
| Hermes Agent | `hermes` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory/tree/main/integrations/hermes. |
| Kiro | `kiro` | Using MCP via ~/.kiro/settings/mcp.json (user-level). Workspace overrides live in .kiro/settings/mcp.json. |
+| Klaat Code | `klaatcode` | Using MCP via ~/.klaatai/mcp.json (key: servers). Pass --with-hooks for native auto-capture. |
| OpenClaw | `openclaw` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory/tree/main/integrations/openclaw. |
| OpenCode | `opencode` | Using MCP via ~/.config/opencode/opencode.json (top-level `mcp` key). For full auto-capture, also install the bundled plugin in plugin/opencode/. |
| OpenHuman | `openhuman` | Using native hooks (REST API at :3111). MCP not required. |
diff --git a/src/cli/connect/guidelines.ts b/src/cli/connect/guidelines.ts
index 26770b6a7..a9614f457 100644
--- a/src/cli/connect/guidelines.ts
+++ b/src/cli/connect/guidelines.ts
@@ -84,6 +84,16 @@ export function guidelineTargets(
scope: "global",
source: "https://kiro.dev/docs/steering",
},
+ // Klaat Code has capture hooks but discards their stdout, so unlike Claude
+ // Code and Codex it has no path for injecting recalled context. Without a
+ // guideline nothing ever prompts the model to call memory_recall.
+ klaatcode: {
+ globalPath: join(home, ".klaatai", "rules.md"),
+ projectPath: join(".klaatai", "rules.md"),
+ format: "block",
+ scope: "global",
+ source: "https://github.com/KlaatAI/klaatcode#project-rules",
+ },
"gemini-cli": {
globalPath: join(home, ".gemini", "GEMINI.md"),
projectPath: "GEMINI.md",
diff --git a/src/cli/connect/index.ts b/src/cli/connect/index.ts
index 981b7d65f..cb0be153c 100644
--- a/src/cli/connect/index.ts
+++ b/src/cli/connect/index.ts
@@ -17,6 +17,7 @@ import { adapter as dsh } from "./dsh.js";
import { adapter as geminiCli } from "./gemini-cli.js";
import { adapter as hermes } from "./hermes.js";
import { adapter as kiro } from "./kiro.js";
+import { adapter as klaatcode } from "./klaatcode.js";
import { adapter as openclaw } from "./openclaw.js";
import { adapter as opencode } from "./opencode.js";
import { adapter as openhuman } from "./openhuman.js";
@@ -36,6 +37,7 @@ export const ADAPTERS: readonly ConnectAdapter[] = [
antigravity,
antigravityCli,
kiro,
+ klaatcode,
warp,
cline,
continueDev,
diff --git a/src/cli/connect/klaatcode-hooks.ts b/src/cli/connect/klaatcode-hooks.ts
new file mode 100644
index 000000000..3252c7b1a
--- /dev/null
+++ b/src/cli/connect/klaatcode-hooks.ts
@@ -0,0 +1,98 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+/**
+ * Separate from `codex-hooks.ts` because Klaat Code's hooks file is flat and
+ * snake_case rather than the nested Claude-Code shape `buildMergedHooks`
+ * handles. Loader: KlaatAI/klaatcode `src/screens/repl.ts`.
+ *
+ * Klaat Code never injects `${CLAUDE_PLUGIN_ROOT}` — the token is the shared
+ * placeholder in every bundled manifest, resolved here so the written file
+ * needs no env expansion.
+ */
+
+export type KlaatcodeHookEvent =
+ | "before_tool"
+ | "after_tool"
+ | "before_message"
+ | "after_message"
+ | "session_start"
+ | "session_end";
+
+export type KlaatcodeHookEntry =
+ | string
+ | { command: string; matcher?: string; timeout?: number };
+
+export type KlaatcodeHooksConfig = Partial<
+ Record
+>;
+
+function entryCommand(entry: KlaatcodeHookEntry): string {
+ return typeof entry === "string" ? entry : entry.command;
+}
+
+function normalizePathForCommandMatch(value: string): string {
+ return value.replace(/\\/g, "/");
+}
+
+function isAgentmemoryEntry(
+ entry: KlaatcodeHookEntry,
+ scriptsDir: string,
+): boolean {
+ return normalizePathForCommandMatch(entryCommand(entry)).includes(
+ normalizePathForCommandMatch(scriptsDir),
+ );
+}
+
+function resolveEntry(
+ entry: KlaatcodeHookEntry,
+ pluginRoot: string,
+): KlaatcodeHookEntry {
+ const command = entryCommand(entry).replace(
+ /\$\{CLAUDE_PLUGIN_ROOT\}/g,
+ pluginRoot,
+ );
+ if (typeof entry === "string") return command;
+ const next: { command: string; matcher?: string; timeout?: number } = {
+ command,
+ };
+ if (entry.matcher !== undefined) next.matcher = entry.matcher;
+ if (entry.timeout !== undefined) next.timeout = entry.timeout;
+ return next;
+}
+
+export function buildMergedKlaatcodeHooks(
+ existing: KlaatcodeHooksConfig | null,
+ pluginRoot: string,
+ manifestFile = "hooks.klaatcode.json",
+): KlaatcodeHooksConfig {
+ const ours = JSON.parse(
+ readFileSync(join(pluginRoot, "hooks", manifestFile), "utf-8"),
+ ) as KlaatcodeHooksConfig;
+ const scriptsDir = join(pluginRoot, "scripts");
+
+ const out: KlaatcodeHooksConfig = {};
+
+ if (existing) {
+ for (const [event, entries] of Object.entries(existing) as [
+ KlaatcodeHookEvent,
+ KlaatcodeHookEntry[],
+ ][]) {
+ if (!Array.isArray(entries)) continue;
+ const kept = entries.filter(
+ (entry) => !isAgentmemoryEntry(entry, scriptsDir),
+ );
+ if (kept.length > 0) out[event] = kept;
+ }
+ }
+
+ for (const [event, entries] of Object.entries(ours) as [
+ KlaatcodeHookEvent,
+ KlaatcodeHookEntry[],
+ ][]) {
+ const resolved = entries.map((entry) => resolveEntry(entry, pluginRoot));
+ out[event] = [...(out[event] ?? []), ...resolved];
+ }
+
+ return out;
+}
diff --git a/src/cli/connect/klaatcode.ts b/src/cli/connect/klaatcode.ts
new file mode 100644
index 000000000..62f184e4d
--- /dev/null
+++ b/src/cli/connect/klaatcode.ts
@@ -0,0 +1,88 @@
+import { existsSync, mkdirSync } from "node:fs";
+import { homedir } from "node:os";
+import { join } from "node:path";
+import * as p from "@clack/prompts";
+import { createJsonMcpAdapter } from "./json-mcp-adapter.js";
+import type { ConnectOptions, ConnectResult } from "./types.js";
+import { findPluginRoot } from "./codex-hooks.js";
+import {
+ buildMergedKlaatcodeHooks,
+ type KlaatcodeHooksConfig,
+} from "./klaatcode-hooks.js";
+import {
+ backupFile,
+ logBackup,
+ logInstalled,
+ readJsonSafe,
+ writeJsonAtomic,
+} from "./util.js";
+
+function klaatDir(): string {
+ const override = process.env["KLAATAI_DIR"];
+ if (override && override.trim()) return override;
+ return join(homedir(), ".klaatai");
+}
+
+const KLAAT_DIR = klaatDir();
+const KLAAT_MCP = join(KLAAT_DIR, "mcp.json");
+const KLAAT_HOOKS = join(KLAAT_DIR, "hooks.json");
+
+export const adapter = createJsonMcpAdapter({
+ name: "klaatcode",
+ displayName: "Klaat Code",
+ detectDir: KLAAT_DIR,
+ configPath: KLAAT_MCP,
+ wrapperKey: "servers",
+ category: "native",
+ docs: "https://github.com/rohitg00/agentmemory#other-agents",
+ protocolNote:
+ "→ Using MCP via ~/.klaatai/mcp.json (key: servers). Pass --with-hooks for native auto-capture.",
+ installHooks: installKlaatcodeHooks,
+});
+
+function installKlaatcodeHooks(opts: ConnectOptions): ConnectResult {
+ let pluginRoot: string;
+ try {
+ pluginRoot = findPluginRoot();
+ } catch (err) {
+ return {
+ kind: "skipped",
+ reason: err instanceof Error ? err.message : String(err),
+ };
+ }
+
+ const existing = readJsonSafe(KLAAT_HOOKS);
+ const merged = buildMergedKlaatcodeHooks(
+ existing,
+ pluginRoot,
+ "hooks.klaatcode.json",
+ );
+
+ if (opts.dryRun) {
+ p.log.info(
+ `[dry-run] Would write ${Object.keys(merged).length} hook event(s) into ${KLAAT_HOOKS}`,
+ );
+ return { kind: "installed", mutatedPath: KLAAT_HOOKS };
+ }
+
+ let backupPath: string | undefined;
+ if (existsSync(KLAAT_HOOKS)) {
+ backupPath = backupFile(KLAAT_HOOKS, "klaatcode-hooks", "json");
+ logBackup(backupPath);
+ } else {
+ mkdirSync(KLAAT_DIR, { recursive: true });
+ }
+
+ writeJsonAtomic(KLAAT_HOOKS, merged);
+
+ logInstalled("Klaat Code hooks", KLAAT_HOOKS);
+ p.log.info(
+ "Verify with `/hooks` inside klaatcode. Hooks fire in the interactive TUI only — headless (`klaatai run`) and ACP sessions are not captured. Re-run `agentmemory connect klaatcode --with-hooks` after upgrading agentmemory so the plugin paths stay current.",
+ );
+
+ return {
+ kind: "installed",
+ mutatedPath: KLAAT_HOOKS,
+ ...(backupPath !== undefined && { backupPath }),
+ };
+}
diff --git a/src/cli/onboarding.ts b/src/cli/onboarding.ts
index 926cdbea9..6d8b8a39f 100644
--- a/src/cli/onboarding.ts
+++ b/src/cli/onboarding.ts
@@ -45,6 +45,7 @@ const AGENT_GLYPH: Record = {
codex: "◎",
cursor: "◫",
"gemini-cli": "✦",
+ klaatcode: "◭",
opencode: "⬡",
};
diff --git a/src/hooks/_project.ts b/src/hooks/_project.ts
index 9f0320c5c..3a030454e 100644
--- a/src/hooks/_project.ts
+++ b/src/hooks/_project.ts
@@ -28,8 +28,16 @@ export function hookCwd(data: Record | null | undefined): strin
if (typeof root === "string" && root.trim()) return root;
}
}
- const projectDir =
- process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"];
- if (projectDir && projectDir.trim()) return projectDir;
+ if (typeof data.project_root === "string" && data.project_root.trim()) {
+ return data.project_root;
+ }
+ for (const name of [
+ "DEVIN_PROJECT_DIR",
+ "CLAUDE_PROJECT_DIR",
+ "KLAATAI_PROJECT_ROOT",
+ ]) {
+ const value = process.env[name];
+ if (value && value.trim()) return value;
+ }
return undefined;
}
diff --git a/test/cli-connect.test.ts b/test/cli-connect.test.ts
index ce1cc3440..4c741634b 100644
--- a/test/cli-connect.test.ts
+++ b/test/cli-connect.test.ts
@@ -57,6 +57,7 @@ describe("agentmemory connect — dispatcher", () => {
"gemini-cli",
"hermes",
"kiro",
+ "klaatcode",
"opencode",
"openclaw",
"openhuman",
@@ -66,7 +67,7 @@ describe("agentmemory connect — dispatcher", () => {
"zed",
].sort(),
);
- expect(ADAPTERS.length).toBe(21);
+ expect(ADAPTERS.length).toBe(22);
});
it("every adapter exposes detect() and install()", () => {
diff --git a/test/connect-guidelines.test.ts b/test/connect-guidelines.test.ts
index 633aedc5e..8023d1586 100644
--- a/test/connect-guidelines.test.ts
+++ b/test/connect-guidelines.test.ts
@@ -29,6 +29,17 @@ describe("writeGuideline", () => {
expect(body).toContain("memory_save");
});
+ it("writes a marked block into Klaat Code's global rules.md", () => {
+ const r = writeGuideline("klaatcode", { cwd, home });
+ expect(r.kind).toBe("written");
+ if (r.kind === "written") expect(r.scope).toBe("global");
+ const path = join(home, ".klaatai", "rules.md");
+ expect(existsSync(path)).toBe(true);
+ const body = readFileSync(path, "utf8");
+ expect(body).toContain("memory_recall");
+ expect(body).toContain("memory_save");
+ });
+
it("writes a Kiro steering file with inclusion: always (global)", () => {
const r = writeGuideline("kiro", { cwd, home });
expect(r.kind).toBe("written");
@@ -141,6 +152,7 @@ describe("guidelineTargets coverage", () => {
"droid",
"gemini-cli",
"kiro",
+ "klaatcode",
"opencode",
"qwen",
"warp",
diff --git a/test/klaatcode-connect-hooks.test.ts b/test/klaatcode-connect-hooks.test.ts
new file mode 100644
index 000000000..195046a1f
--- /dev/null
+++ b/test/klaatcode-connect-hooks.test.ts
@@ -0,0 +1,154 @@
+import { describe, it, expect } from "vitest";
+import { resolve } from "node:path";
+import { spawn } from "node:child_process";
+import { findPluginRoot } from "../src/cli/connect/codex-hooks.js";
+import {
+ buildMergedKlaatcodeHooks,
+ type KlaatcodeHooksConfig,
+} from "../src/cli/connect/klaatcode-hooks.js";
+
+const PLUGIN_ROOT = resolve(__dirname, "..", "plugin");
+
+const KLAATCODE_EVENTS = [
+ "session_start",
+ "before_message",
+ "before_tool",
+ "after_tool",
+ "after_message",
+ "session_end",
+];
+
+const KLAATCODE_TOOL_NAMES = [
+ "run_command",
+ "edit_file",
+ "multi_edit",
+ "write_file",
+ "read_file",
+ "apply_patch",
+ "grep",
+ "glob",
+];
+
+function entryCommand(entry: string | { command: string }): string {
+ return typeof entry === "string" ? entry : entry.command;
+}
+
+function runHook(
+ script: string,
+ payload: Record,
+ env: Record = {},
+): Promise<{ out: string; code: number | null }> {
+ return new Promise((resolve_) => {
+ const child = spawn("node", [`plugin/scripts/${script}.mjs`], {
+ env: { ...process.env, AGENTMEMORY_URL: "http://127.0.0.1:1", ...env },
+ });
+ let out = "";
+ child.stdout.on("data", (c) => (out += c));
+ child.on("exit", (code) => resolve_({ out, code }));
+ child.stdin.write(JSON.stringify(payload));
+ child.stdin.end();
+ });
+}
+
+describe("buildMergedKlaatcodeHooks (Klaat Code manifest)", () => {
+ it("rewrites ${CLAUDE_PLUGIN_ROOT} to absolute pluginRoot in every command", () => {
+ const merged = buildMergedKlaatcodeHooks(null, findPluginRoot());
+ for (const entries of Object.values(merged)) {
+ for (const entry of entries!) {
+ const cmd = entryCommand(entry);
+ expect(cmd).not.toContain("${CLAUDE_PLUGIN_ROOT}");
+ expect(cmd).toContain(`${PLUGIN_ROOT}/scripts/`);
+ }
+ }
+ });
+
+ it("wires exactly the six events Klaat Code dispatches", () => {
+ const merged = buildMergedKlaatcodeHooks(null, findPluginRoot());
+ expect(Object.keys(merged).sort()).toEqual([...KLAATCODE_EVENTS].sort());
+ });
+
+ it("matches Klaat Code's snake_case tool names, not Claude Code's", () => {
+ const merged = buildMergedKlaatcodeHooks(null, findPluginRoot());
+ const entry = merged["before_tool"]?.[0];
+ expect(typeof entry).toBe("object");
+ const matcher = (entry as { matcher?: string }).matcher;
+ expect(matcher).toBeDefined();
+ const re = new RegExp(matcher!);
+ for (const tool of KLAATCODE_TOOL_NAMES) expect(re.test(tool)).toBe(true);
+ for (const claudeTool of ["Edit", "Write", "Bash"]) {
+ expect(re.test(claudeTool)).toBe(false);
+ }
+ });
+
+ it("preserves user-authored hooks in both v1 and v2 form", () => {
+ const existing: KlaatcodeHooksConfig = {
+ after_message: ["afplay /System/Library/Sounds/Glass.aiff"],
+ before_tool: [{ command: "./scripts/guard-shell.sh", matcher: "run_command" }],
+ };
+ const merged = buildMergedKlaatcodeHooks(existing, findPluginRoot());
+ expect(merged["after_message"]?.[0]).toBe(
+ "afplay /System/Library/Sounds/Glass.aiff",
+ );
+ expect(entryCommand(merged["before_tool"]![0]!)).toBe(
+ "./scripts/guard-shell.sh",
+ );
+ });
+
+ it("is idempotent across re-installs", () => {
+ const root = findPluginRoot();
+ const once = buildMergedKlaatcodeHooks(null, root);
+ const twice = buildMergedKlaatcodeHooks(once, root);
+ expect(twice).toEqual(once);
+ });
+});
+
+describe("hook payload compatibility with Klaat Code", () => {
+ it("resolves the project from the payload's project_root field", async () => {
+ const { resolveProject, hookCwd } = await import("../src/hooks/_project.js");
+ const payload = { session_id: "s1", project_root: "/tmp" };
+ expect(hookCwd(payload)).toBe("/tmp");
+ expect(resolveProject(hookCwd(payload))).toBe("tmp");
+ });
+
+ it("falls back to KLAATAI_PROJECT_ROOT when the payload carries no path", async () => {
+ const { hookCwd } = await import("../src/hooks/_project.js");
+ const before = process.env["KLAATAI_PROJECT_ROOT"];
+ process.env["KLAATAI_PROJECT_ROOT"] = "/tmp";
+ try {
+ expect(hookCwd({ session_id: "s1" })).toBe("/tmp");
+ } finally {
+ if (before === undefined) delete process.env["KLAATAI_PROJECT_ROOT"];
+ else process.env["KLAATAI_PROJECT_ROOT"] = before;
+ }
+ });
+
+ it("skips a blank earlier env var instead of letting it shadow the fallback", async () => {
+ const { hookCwd } = await import("../src/hooks/_project.js");
+ const before = {
+ devin: process.env["DEVIN_PROJECT_DIR"],
+ klaat: process.env["KLAATAI_PROJECT_ROOT"],
+ };
+ process.env["DEVIN_PROJECT_DIR"] = " ";
+ process.env["KLAATAI_PROJECT_ROOT"] = "/tmp";
+ try {
+ expect(hookCwd({ session_id: "s1" })).toBe("/tmp");
+ } finally {
+ for (const [key, value] of [
+ ["DEVIN_PROJECT_DIR", before.devin],
+ ["KLAATAI_PROJECT_ROOT", before.klaat],
+ ] as const) {
+ if (value === undefined) delete process.env[key];
+ else process.env[key] = value;
+ }
+ }
+ });
+
+ it("fails open when the memory server is unreachable", async () => {
+ const { code } = await runHook("session-start", {
+ event: "session_start",
+ session_id: "s1",
+ project_root: "/tmp",
+ });
+ expect(code).toBe(0);
+ });
+});
diff --git a/website/components/Agents.tsx b/website/components/Agents.tsx
index d8f4fa3ca..2cf05e6be 100644
--- a/website/components/Agents.tsx
+++ b/website/components/Agents.tsx
@@ -233,6 +233,14 @@ const MARQUEE: Agent[] = [
accent: "#317CFF",
href: "https://devin.ai",
},
+ {
+ id: "klaatcode",
+ name: "Klaat Code",
+ from: "KlaatAI",
+ logo: "/klaatcode.png",
+ accent: "#8B5CF6",
+ href: "https://github.com/KlaatAI/klaatcode",
+ },
];
function FeaturedCard({ a }: { a: Agent }) {
diff --git a/website/public/klaatcode.png b/website/public/klaatcode.png
new file mode 100644
index 000000000..5412271c2
Binary files /dev/null and b/website/public/klaatcode.png differ