Skip to content

fix(claude-code): --with-hooks for MCP-standalone users (closes #508)#581

Open
rohitg00 wants to merge 2 commits into
mainfrom
fix/claude-code-with-hooks
Open

fix(claude-code): --with-hooks for MCP-standalone users (closes #508)#581
rohitg00 wants to merge 2 commits into
mainfrom
fix/claude-code-with-hooks

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented May 20, 2026

Summary

Closes #508.

When agentmemory is wired into Claude Code via the MCP-standalone path (registering @agentmemory/mcp in ~/.claude.json rather than running /plugin install agentmemory), Claude Code never resolves ${CLAUDE_PLUGIN_ROOT}. To get auto-capture working, users have to copy hook commands into ~/.claude/settings.json with absolute paths — and those paths typically embed the agentmemory version (e.g. ~/.codex/plugins/cache/agentmemory/agentmemory/0.9.18/scripts/…). Every npm upgrade silently breaks all 9 hooks while MCP keeps working, so memory_diagnose returns all-green and the regression is invisible.

@jonathanzhan1975 quantified the impact on the 0.9.18 → 0.9.20 bump:

all 9 hook events silently dead between the version bump and the moment I happened to audit ~/.claude/settings.json by hand. memory_diagnose returned all-green the whole time.

Fix

Adds the same --with-hooks opt-in the codex adapter already has (#564):

agentmemory connect claude-code --with-hooks
  • Resolves ${CLAUDE_PLUGIN_ROOT} to the absolute bundled plugin/ path at install time.
  • Merges entries from plugin/hooks/hooks.json into ~/.claude/settings.json's top-level hooks field.
  • Re-install strips previous agentmemory entries by detecting commands under <pluginRoot>/scripts/; unrelated user hooks survive untouched.
  • Runs even when MCP is already wired (already-wired early return now also calls the hooks branch when --with-hooks is passed).
  • --dry-run --with-hooks previews the change.

Mechanism

Reuses findPluginRoot + buildMergedHooks from codex-hooks.ts. buildMergedHooks now takes the manifest filename explicitly:

  • hooks.codex.json for the codex adapter (Codex's 6-event subset)
  • hooks.json for the claude-code adapter (full 12-event Claude set including SessionEnd, SubagentStop, Notification, TaskCompleted, PostToolUseFailure)

Diff

  • src/cli/connect/claude-code.ts+86 adds installClaudeHooks() + wire-up
  • src/cli/connect/codex-hooks.ts+5/-2 parametrize the manifest filename
  • test/claude-code-with-hooks.test.ts+72 new, 5 unit tests
  • README.md+12 new "Claude Code without the plugin install" subsection

Validation

  • npm test → 98/98 test files, 1086/1086 tests pass
  • npm run build → bundle clean
  • Smoke test: node dist/cli.mjs connect claude-code --dry-run --with-hooks previews 12 events, runs even when MCP is already wired.

Recommended user flow

The README block points at the proper /plugin marketplace add + /plugin install path first; --with-hooks is the explicit escape hatch for setups that can't or won't use the Claude plugin install.

Summary by CodeRabbit

  • Documentation

    • Added troubleshooting guidance for Claude Code hook installation and configuration.
    • Updated Codex CLI plugin marketplace setup instructions.
  • New Features

    • Added hook installation support with configurable options for Claude Code setup.
  • Tests

    • Expanded test coverage for hook configuration and merging scenarios.

Review Change Stack

rohitg00 added 2 commits May 20, 2026 17:17
Codex's plugin CLI (codex-rs/cli/src/plugin_cmd.rs) exposes:

  codex plugin add <PLUGIN>[@<MARKETPLACE>]
  codex plugin list
  codex plugin remove <PLUGIN>
  codex plugin marketplace <add|upgrade|remove>

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.
…standalone users

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 `<pluginRoot>/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.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentmemory Ready Ready Preview, Comment May 20, 2026 4:23pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

This PR addresses version-lock issues in Claude Code when using MCP standalone (without the plugin loader) by adding a --with-hooks option to merge hook entries from bundled manifests into ~/.claude/settings.json with absolute paths that survive version updates.

Changes

Claude Code hook installation

Layer / File(s) Summary
Hook merging utility refactoring
src/cli/connect/codex-hooks.ts
buildMergedHooks now accepts an optional manifestFile parameter (defaulting to "hooks.codex.json") so the same merge logic can load different bundled hook manifests for Claude Code and Codex variants.
Claude Code hook installation and tests
src/cli/connect/claude-code.ts, test/claude-code-with-hooks.test.ts
The claude-code adapter adds an --withHooks option that triggers installClaudeHooks() to merge bundled plugin/hooks/hooks.json into ~/.claude/settings.json with absolute paths, supporting dry-run, backup, and atomic writes. Tests verify path substitution from ${CLAUDE_PLUGIN_ROOT} to concrete paths, presence of Claude-only events, preservation of user hooks, and idempotency across re-merges.
Documentation and user-facing messaging
README.md, src/cli/connect/codex.ts
README adds a Claude Code MCP-standalone troubleshooting section explaining the --with-hooks workaround for absolute path issues and notes that upgrades require re-running the command. Codex CLI instructions updated from codex plugin install to codex plugin add agentmemory@agentmemory marketplace flow, and the success message for Codex CLI install reflects the new command.

Possibly related PRs

  • rohitg00/agentmemory#564: Both PRs modify src/cli/connect/codex-hooks.ts's buildMergedHooks logic/signature to make it more flexible for different manifest sources.

  • rohitg00/agentmemory#487: Both PRs handle hook command paths and their script-path references; the main PR adds the merge/install flow while the retrieved PR updates the bundled hook manifests themselves.

  • rohitg00/agentmemory#311: The main PR's buildMergedHooks is directly tied to PR #311's addition of plugin/hooks/hooks.codex.json and Codex marketplace packaging.

🐰 A Claude hook by any manifest name,
Now absolute paths tame the version game!
No more silent breaks on upgrade day,
The --with-hooks shows the scripted way.


🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(claude-code): --with-hooks for MCP-standalone users (closes #508)' directly and clearly summarizes the main change: adding a --with-hooks option for Claude Code MCP-standalone users to fix hook breakage.
Linked Issues check ✅ Passed The PR fully implements the third fix idea from issue #508: a CLI helper (--with-hooks) that resolves plugin root at install time, merges hooks into settings.json, and is idempotent for re-running after upgrades.
Out of Scope Changes check ✅ Passed All changes are scoped to the --with-hooks feature for Claude Code, hook manifest parameterization for codex-hooks reuse, documentation updates, and related tests; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/claude-code-with-hooks

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/connect/claude-code.ts (1)

74-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

--with-hooks is skipped on fresh --dry-run installs.

Line 74 returns before running hook preview, so agentmemory connect claude-code --dry-run --with-hooks does not show hook changes unless MCP is already wired.

Suggested fix
     if (opts.dryRun) {
       p.log.info(
         `[dry-run] Would ${alreadyHas ? "overwrite" : "add"} mcpServers.agentmemory in ${CLAUDE_JSON}`,
       );
+      if (opts.withHooks) {
+        const hookResult = installClaudeHooks(opts);
+        if (hookResult.kind === "skipped") {
+          p.log.warn(`Claude Code hooks fallback skipped: ${hookResult.reason}.`);
+        }
+      }
       return { kind: "installed", mutatedPath: CLAUDE_JSON };
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/connect/claude-code.ts` around lines 74 - 79, The dry-run branch
returns early (when opts.dryRun) and skips the hook preview, so running
`agentmemory connect claude-code --dry-run --with-hooks` won’t show hook
changes; modify the block around opts.dryRun / alreadyHas / CLAUDE_JSON to not
return before running the hook-preview logic: after logging the `[dry-run] Would
...` message, if opts.withHooks (the CLI flag for --with-hooks) is set, invoke
the same hook preview function/path used elsewhere in this file (the code that
computes and displays hook changes) and then return the { kind: "installed",
mutatedPath: CLAUDE_JSON } result; alternatively move the hook-preview
invocation before the return so dry-run shows hooks for fresh installs as well.
🧹 Nitpick comments (1)
test/claude-code-with-hooks.test.ts (1)

34-37: ⚡ Quick win

Strengthen Claude-only event assertion to prevent partial regressions.

Using some(...) allows this test to pass when only one Claude-only event exists; assert each expected event explicitly.

Suggested refactor
-    expect(
-      claudeOnly.some((e) => events.includes(e)),
-      `hooks.json should include at least one Claude-only event (${claudeOnly.join(", ")})`,
-    ).toBe(true);
+    for (const event of claudeOnly) {
+      expect(events).toContain(event);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/claude-code-with-hooks.test.ts` around lines 34 - 37, The test currently
uses claudeOnly.some(...) which only verifies that at least one Claude-only
event is present; change it to assert all expected Claude-only events are
included by using claudeOnly.every(e => events.includes(e)) and update the
failure message to reflect that every Claude-only event must appear (reference
symbols: claudeOnly, events, the expect(...).toBe(true) assertion). Ensure the
assertion logic is replaced so the test fails if any single expected Claude-only
event is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/connect/claude-code.ts`:
- Around line 131-147: installClaudeHooks currently only catches errors from
findPluginRoot; any failures while reading/parsing CLAUDE_SETTINGS, building
merged hooks via buildMergedHooks, or writing settings will throw and abort the
connect flow—wrap the risky operations (readJsonSafe(CLAUDE_SETTINGS),
buildMergedHooks(existingHooks, pluginRoot, "hooks.json"), and the settings
write path) in try/catch blocks and on error return a ConnectResult with kind:
"skipped" and reason set to the error message (preserve existing pattern err
instanceof Error ? err.message : String(err)); apply the same defensive handling
to the analogous code around the 164-166 region so manifest merge/write failures
become non-fatal warnings instead of exceptions.

---

Outside diff comments:
In `@src/cli/connect/claude-code.ts`:
- Around line 74-79: The dry-run branch returns early (when opts.dryRun) and
skips the hook preview, so running `agentmemory connect claude-code --dry-run
--with-hooks` won’t show hook changes; modify the block around opts.dryRun /
alreadyHas / CLAUDE_JSON to not return before running the hook-preview logic:
after logging the `[dry-run] Would ...` message, if opts.withHooks (the CLI flag
for --with-hooks) is set, invoke the same hook preview function/path used
elsewhere in this file (the code that computes and displays hook changes) and
then return the { kind: "installed", mutatedPath: CLAUDE_JSON } result;
alternatively move the hook-preview invocation before the return so dry-run
shows hooks for fresh installs as well.

---

Nitpick comments:
In `@test/claude-code-with-hooks.test.ts`:
- Around line 34-37: The test currently uses claudeOnly.some(...) which only
verifies that at least one Claude-only event is present; change it to assert all
expected Claude-only events are included by using claudeOnly.every(e =>
events.includes(e)) and update the failure message to reflect that every
Claude-only event must appear (reference symbols: claudeOnly, events, the
expect(...).toBe(true) assertion). Ensure the assertion logic is replaced so the
test fails if any single expected Claude-only event is missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a26ee70-5963-4d8f-b662-229dcd557be2

📥 Commits

Reviewing files that changed from the base of the PR and between edd1ceb and a43a279.

📒 Files selected for processing (5)
  • README.md
  • src/cli/connect/claude-code.ts
  • src/cli/connect/codex-hooks.ts
  • src/cli/connect/codex.ts
  • test/claude-code-with-hooks.test.ts

Comment on lines +131 to +147
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<ClaudeSettings>(CLAUDE_SETTINGS) ?? {};
const existingHooks: HookManifest | null = existing.hooks
? { hooks: existing.hooks }
: null;
const merged = buildMergedHooks(existingHooks, pluginRoot, "hooks.json");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make hook fallback non-fatal by catching merge/write failures.

installClaudeHooks() only guards findPluginRoot(). If manifest read/parse or settings write fails, it throws and can abort the whole connect flow instead of returning kind: "skipped" for the warning path.

Suggested fix
 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<ClaudeSettings>(CLAUDE_SETTINGS) ?? {};
-  const existingHooks: HookManifest | null = existing.hooks
-    ? { hooks: existing.hooks }
-    : null;
-  const merged = buildMergedHooks(existingHooks, pluginRoot, "hooks.json");
+  try {
+    type ClaudeSettings = { hooks?: HookManifest["hooks"]; [key: string]: unknown };
+    const existing = readJsonSafe<ClaudeSettings>(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 };
-  }
+    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 });
-  }
+    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);
+    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.",
-  );
+    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 }),
-  };
+    return {
+      kind: "installed",
+      mutatedPath: CLAUDE_SETTINGS,
+      ...(backupPath !== undefined && { backupPath }),
+    };
+  } catch (err) {
+    return {
+      kind: "skipped",
+      reason: err instanceof Error ? err.message : String(err),
+    };
+  }
 }

Also applies to: 164-166

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/connect/claude-code.ts` around lines 131 - 147, installClaudeHooks
currently only catches errors from findPluginRoot; any failures while
reading/parsing CLAUDE_SETTINGS, building merged hooks via buildMergedHooks, or
writing settings will throw and abort the connect flow—wrap the risky operations
(readJsonSafe(CLAUDE_SETTINGS), buildMergedHooks(existingHooks, pluginRoot,
"hooks.json"), and the settings write path) in try/catch blocks and on error
return a ConnectResult with kind: "skipped" and reason set to the error message
(preserve existing pattern err instanceof Error ? err.message : String(err));
apply the same defensive handling to the analogous code around the 164-166
region so manifest merge/write failures become non-fatal warnings instead of
exceptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hooks break on every version bump when agentmemory is wired into Claude Code outside the plugin loader

1 participant