From e9da107887b8c7dd709683ce04687b748763c8f4 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Wed, 20 May 2026 17:17:05 +0100 Subject: [PATCH 1/2] fix(docs): correct codex plugin install command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex's plugin CLI (codex-rs/cli/src/plugin_cmd.rs) exposes: codex plugin add [@] codex plugin list codex plugin remove codex plugin marketplace It does NOT have a `codex plugin install` subcommand. Users following the README would hit: error: unrecognized subcommand 'install' Fix two callsites that documented the wrong command: - README.md: top-level Codex install snippet + agent matrix row - src/cli/connect/codex.ts: final info hint after `agentmemory connect codex` CHANGELOG history is left alone since those entries reference what the README said at the time and rewriting history is not the right move. Slash-command references like `/plugin install agentmemory` inside Claude Code are unrelated and remain — that's Claude Code's slash API, not the codex CLI. --- README.md | 4 ++-- src/cli/connect/codex.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 840a75c4..9a511204 100644 --- a/README.md +++ b/README.md @@ -419,7 +419,7 @@ npx @agentmemory/agentmemory # 2. register the agentmemory marketplace and install the plugin codex plugin marketplace add rohitg00/agentmemory -codex plugin install agentmemory +codex plugin add agentmemory@agentmemory ``` The Codex plugin ships from the same `plugin/` directory as the Claude Code plugin. It registers: @@ -516,7 +516,7 @@ The agentmemory entry is the **same MCP server block** across every host that us | **Gemini CLI** | `~/.gemini/settings.json` | `gemini mcp add agentmemory npx -y @agentmemory/mcp --scope user` (auto-merges). | | **OpenClaw** | OpenClaw MCP config | Same `mcpServers` block, or use the deeper [memory plugin](integrations/openclaw/). | | **Codex CLI (MCP only)** | `.codex/config.toml` | TOML shape: `codex mcp add agentmemory -- npx -y @agentmemory/mcp`, or add `[mcp_servers.agentmemory]` manually. | -| **Codex CLI (full plugin)** | Codex plugin marketplace | `codex plugin marketplace add rohitg00/agentmemory` then `codex plugin install agentmemory`. Registers MCP + 6 lifecycle hooks (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Stop) + 4 skills. On Codex Desktop, also run `agentmemory connect codex --with-hooks` until [openai/codex#16430](https://github.com/openai/codex/issues/16430) lands — plugin hooks are currently silent there. | +| **Codex CLI (full plugin)** | Codex plugin marketplace | `codex plugin marketplace add rohitg00/agentmemory` then `codex plugin add agentmemory@agentmemory`. Registers MCP + 6 lifecycle hooks (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Stop) + 4 skills. On Codex Desktop, also run `agentmemory connect codex --with-hooks` until [openai/codex#16430](https://github.com/openai/codex/issues/16430) lands — plugin hooks are currently silent there. | | **OpenCode (MCP only)** | `opencode.json` | Different shape — top-level `mcp` key, command as array: `{"mcp": {"agentmemory": {"type": "local", "command": ["npx", "-y", "@agentmemory/mcp"], "enabled": true}}}`. | | **OpenCode (full plugin)** | `plugin/opencode/` | 22 auto-capture hooks covering session lifecycle, messages, tools, errors. Two slash commands (`/recall`, `/remember`). Copy `plugin/opencode/` into your OpenCode workspace and add the plugin entry to `opencode.json`. See [`plugin/opencode/README.md`](plugin/opencode/README.md) for the full hook table + gap analysis. | | **pi** | `~/.pi/agent/extensions/agentmemory` | Copy [`integrations/pi`](integrations/pi/) and restart pi. | diff --git a/src/cli/connect/codex.ts b/src/cli/connect/codex.ts index a87b2858..c85808da 100644 --- a/src/cli/connect/codex.ts +++ b/src/cli/connect/codex.ts @@ -112,7 +112,7 @@ export const adapter: ConnectAdapter = { logInstalled("Codex CLI", CODEX_TOML); p.log.info( - "Codex picks up MCP servers on next launch. For the deeper plugin install, run: codex plugin marketplace add rohitg00/agentmemory && codex plugin install agentmemory", + "Codex picks up MCP servers on next launch. For the deeper plugin install, run: codex plugin marketplace add rohitg00/agentmemory && codex plugin add agentmemory@agentmemory", ); if (opts.withHooks) { From a43a27996aa1c9f8bfeb3e886ff8f865a05cbab2 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Wed, 20 May 2026 17:22:44 +0100 Subject: [PATCH 2/2] fix(claude-code): --with-hooks installs version-stable hooks for MCP-standalone users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When agentmemory is wired into Claude Code through the MCP-standalone path (registering `@agentmemory/mcp` in `~/.claude.json` rather than running `/plugin install agentmemory`), Claude Code never resolves `${CLAUDE_PLUGIN_ROOT}`. Users have to copy hook commands into `~/.claude/settings.json` with absolute paths, and those paths embed the agentmemory version. Every npm upgrade silently breaks all 9 hooks — MCP keeps working, so `memory_diagnose` stays green while auto-capture is dead. Adds the same `--with-hooks` flag the codex adapter already has: agentmemory connect claude-code --with-hooks Resolves `${CLAUDE_PLUGIN_ROOT}` to the absolute bundled plugin/ path at install time and merges the entries from `plugin/hooks/hooks.json` into `~/.claude/settings.json`'s top-level `hooks` field. Idempotent re-install via the `/scripts/` substring (mirrors the codex-hooks pattern from #564). Unrelated user hooks survive. Reuses `findPluginRoot` + `buildMergedHooks` from codex-hooks.ts; `buildMergedHooks` now takes an explicit manifest filename (`hooks.codex.json` for codex, `hooks.json` for claude-code) so the Claude-only events (`SessionEnd`, `SubagentStop`, `Notification`, `TaskCompleted`, `PostToolUseFailure`) come through. Also makes the adapter run the hooks branch even when MCP is already wired — hooks and MCP are independent surfaces. README: new "Claude Code without the plugin install" subsection documents the trade-off + points at the recommended `/plugin install` path first. Tests (1086) + build pass; 5 new unit tests cover Claude-event presence, path rewrite, user-hook preservation, idempotent re-install. --- README.md | 12 ++++ src/cli/connect/claude-code.ts | 86 +++++++++++++++++++++++++++++ src/cli/connect/codex-hooks.ts | 5 +- test/claude-code-with-hooks.test.ts | 74 +++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 test/claude-code-with-hooks.test.ts diff --git a/README.md b/README.md index 9a511204..37abeb87 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,18 @@ Implementation details live in `src/cli.ts` (see `runUpgrade` around the `src/cl Install agentmemory: run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server. Then run `/plugin marketplace add rohitg00/agentmemory` and `/plugin install agentmemory` — the plugin registers all 12 hooks, 4 skills, AND auto-wires the `@agentmemory/mcp` stdio server via its `.mcp.json`, so you get 53 MCP tools (memory_smart_search, memory_save, memory_sessions, memory_governance_delete, etc.) without any extra config step. Verify with `curl http://localhost:3111/agentmemory/health`. The real-time viewer is at http://localhost:3113. ``` +#### Claude Code without the plugin install (MCP-standalone path) + +If you wire agentmemory's MCP server through `~/.claude.json` directly instead of using `/plugin install`, Claude Code never resolves `${CLAUDE_PLUGIN_ROOT}` and you have to point hook scripts at absolute paths in `~/.claude/settings.json`. Those paths typically embed the agentmemory version (e.g. `~/.codex/plugins/cache/agentmemory/agentmemory/0.9.21/scripts/…`), so the next upgrade silently breaks every hook ([#508](https://github.com/rohitg00/agentmemory/issues/508)). + +Workaround: + +```bash +agentmemory connect claude-code --with-hooks +``` + +This merges the same hook commands into `~/.claude/settings.json` with absolute paths resolved to the bundled `plugin/` directory of the currently installed `@agentmemory/agentmemory` package. Re-run the command after upgrading agentmemory to refresh the paths. User entries in the same file are preserved; only previous agentmemory entries are replaced. Using the `/plugin install` path remains the recommended approach. + ### Codex CLI (Codex plugin platform) ```bash diff --git a/src/cli/connect/claude-code.ts b/src/cli/connect/claude-code.ts index 9f196e7c..76df1492 100644 --- a/src/cli/connect/claude-code.ts +++ b/src/cli/connect/claude-code.ts @@ -12,9 +12,15 @@ import { readJsonSafe, writeJsonAtomic, } from "./util.js"; +import { + buildMergedHooks, + findPluginRoot, + type HookManifest, +} from "./codex-hooks.js"; const CLAUDE_DIR = join(homedir(), ".claude"); const CLAUDE_JSON = join(homedir(), ".claude.json"); +const CLAUDE_SETTINGS = join(CLAUDE_DIR, "settings.json"); type ClaudeMcpEntry = typeof AGENTMEMORY_MCP_BLOCK; type ClaudeConfig = { @@ -51,6 +57,17 @@ export const adapter: ConnectAdapter = { const alreadyHas = entryMatches(servers["agentmemory"]); if (alreadyHas && !opts.force) { logAlreadyWired("Claude Code", CLAUDE_JSON); + // --with-hooks is independent of MCP wiring (issue #508). Run the + // hooks fallback even when MCP is already in place so users with a + // healthy MCP setup can still pick up version-stable hook paths. + if (opts.withHooks) { + const hookResult = installClaudeHooks(opts); + if (hookResult.kind === "skipped") { + p.log.warn( + `Claude Code hooks fallback skipped: ${hookResult.reason}.`, + ); + } + } return { kind: "already-wired", mutatedPath: CLAUDE_JSON }; } @@ -86,6 +103,75 @@ export const adapter: ConnectAdapter = { p.log.info( "Restart Claude Code (or run `/mcp` inside a session) to pick up the new server.", ); + + if (opts.withHooks) { + const hookResult = installClaudeHooks(opts); + if (hookResult.kind === "skipped") { + p.log.warn( + `Claude Code hooks fallback skipped: ${hookResult.reason}. MCP wiring still applied.`, + ); + } + } + return { kind: "installed", mutatedPath: CLAUDE_JSON, backupPath }; }, }; + +/** + * Merge the bundled `plugin/hooks/hooks.json` into + * `~/.claude/settings.json`'s top-level `hooks` field with absolute + * script paths. Use this when agentmemory is NOT installed through + * `/plugin marketplace add` (e.g. MCP standalone wiring), so the + * hook scripts survive version bumps without `${CLAUDE_PLUGIN_ROOT}` + * expansion (issue #508). + * + * Re-install strips entries whose command points under + * `/scripts/`; unrelated user hook entries survive. + */ +function installClaudeHooks(opts: ConnectOptions): ConnectResult { + let pluginRoot: string; + try { + pluginRoot = findPluginRoot(); + } catch (err) { + return { + kind: "skipped", + reason: err instanceof Error ? err.message : String(err), + }; + } + + type ClaudeSettings = { hooks?: HookManifest["hooks"]; [key: string]: unknown }; + const existing = readJsonSafe(CLAUDE_SETTINGS) ?? {}; + const existingHooks: HookManifest | null = existing.hooks + ? { hooks: existing.hooks } + : null; + const merged = buildMergedHooks(existingHooks, pluginRoot, "hooks.json"); + + if (opts.dryRun) { + p.log.info( + `[dry-run] Would merge agentmemory hook entries into ${CLAUDE_SETTINGS} (${Object.keys(merged.hooks).length} event(s))`, + ); + return { kind: "installed", mutatedPath: CLAUDE_SETTINGS }; + } + + let backupPath: string | undefined; + if (existsSync(CLAUDE_SETTINGS)) { + backupPath = backupFile(CLAUDE_SETTINGS, "claude-settings", "json"); + logBackup(backupPath); + } else { + mkdirSync(CLAUDE_DIR, { recursive: true }); + } + + const next: ClaudeSettings = { ...existing, hooks: merged.hooks }; + writeJsonAtomic(CLAUDE_SETTINGS, next); + + logInstalled("Claude Code hooks (workaround for #508)", CLAUDE_SETTINGS); + p.log.info( + "User-scope hook entries reference absolute paths under the bundled plugin/ dir. Re-run `agentmemory connect claude-code --with-hooks` after upgrading agentmemory to refresh them.", + ); + + return { + kind: "installed", + mutatedPath: CLAUDE_SETTINGS, + ...(backupPath !== undefined && { backupPath }), + }; +} diff --git a/src/cli/connect/codex-hooks.ts b/src/cli/connect/codex-hooks.ts index 679ec8be..2f17715f 100644 --- a/src/cli/connect/codex-hooks.ts +++ b/src/cli/connect/codex-hooks.ts @@ -64,9 +64,10 @@ export function findPluginRoot(startUrl: string = import.meta.url): string { export function buildMergedHooks( existing: HookManifest | null, pluginRoot: string, + manifestFile = "hooks.codex.json", ): HookManifest { - const codexManifestPath = join(pluginRoot, "hooks", "hooks.codex.json"); - const ours = JSON.parse(readFileSync(codexManifestPath, "utf-8")) as HookManifest; + const bundledManifestPath = join(pluginRoot, "hooks", manifestFile); + const ours = JSON.parse(readFileSync(bundledManifestPath, "utf-8")) as HookManifest; const scriptsDir = join(pluginRoot, "scripts"); const out: HookManifest = { hooks: {} }; diff --git a/test/claude-code-with-hooks.test.ts b/test/claude-code-with-hooks.test.ts new file mode 100644 index 00000000..6c36b096 --- /dev/null +++ b/test/claude-code-with-hooks.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from "vitest"; +import { resolve } from "node:path"; +import { + buildMergedHooks, + findPluginRoot, + type HookManifest, +} from "../src/cli/connect/codex-hooks.js"; + +const PLUGIN_ROOT = resolve(__dirname, "..", "plugin"); + +describe("buildMergedHooks against plugin/hooks/hooks.json (Claude Code)", () => { + it("locates the same plugin root used by the codex variant", () => { + expect(findPluginRoot()).toBe(PLUGIN_ROOT); + }); + + it("rewrites ${CLAUDE_PLUGIN_ROOT} to absolute pluginRoot in every command", () => { + const merged = buildMergedHooks(null, PLUGIN_ROOT, "hooks.json"); + for (const entries of Object.values(merged.hooks)) { + for (const entry of entries) { + for (const handler of entry.hooks) { + expect(handler.command).not.toContain("${CLAUDE_PLUGIN_ROOT}"); + expect(handler.command).toContain(`${PLUGIN_ROOT}/scripts/`); + } + } + } + }); + + it("includes Claude-only events that hooks.codex.json omits", () => { + const merged = buildMergedHooks(null, PLUGIN_ROOT, "hooks.json"); + const events = Object.keys(merged.hooks); + expect(events).toContain("SessionStart"); + expect(events).toContain("Stop"); + const claudeOnly = ["SessionEnd", "SubagentStop", "Notification"]; + expect( + claudeOnly.some((e) => events.includes(e)), + `hooks.json should include at least one Claude-only event (${claudeOnly.join(", ")})`, + ).toBe(true); + }); + + it("appends to existing user hooks without dropping them", () => { + const existing: HookManifest = { + hooks: { + SessionStart: [ + { hooks: [{ type: "command", command: "echo user-custom-claude" }] }, + ], + }, + }; + const merged = buildMergedHooks(existing, PLUGIN_ROOT, "hooks.json"); + const sessionStart = merged.hooks["SessionStart"]!; + expect( + sessionStart.some((e) => + e.hooks.some((h) => h.command === "echo user-custom-claude"), + ), + ).toBe(true); + expect( + sessionStart.some((e) => + e.hooks.some((h) => + h.command.includes(`${PLUGIN_ROOT}/scripts/session-start.mjs`), + ), + ), + ).toBe(true); + }); + + it("re-install strips previous agentmemory entries (idempotent)", () => { + const first = buildMergedHooks(null, PLUGIN_ROOT, "hooks.json"); + const second = buildMergedHooks(first, PLUGIN_ROOT, "hooks.json"); + for (const event of Object.keys(first.hooks)) { + expect( + second.hooks[event]!.length, + `${event} should not double after second install`, + ).toBe(first.hooks[event]!.length); + } + }); +});