Skip to content

fix(hooks): merge hooks config into settings.json for claude target#961

Open
shixi-li wants to merge 2 commits intoaffaan-m:mainfrom
shixi-li:fix/claude-hooks-settings-merge
Open

fix(hooks): merge hooks config into settings.json for claude target#961
shixi-li wants to merge 2 commits intoaffaan-m:mainfrom
shixi-li:fix/claude-hooks-settings-merge

Conversation

@shixi-li
Copy link
Copy Markdown

@shixi-li shixi-li commented Mar 27, 2026

Summary

The installer copies hooks/hooks.json to ~/.claude/hooks/hooks.json, but Claude Code only reads hooks from ~/.claude/settings.json. This means all 28 hooks (PreToolUse, PostToolUse, Stop, SessionStart, etc.) are silently inactive after installation for the claude target.

This fix adds a post-install merge step in applyInstallPlan() that writes the hooks field from hooks.json into settings.json, preserving any existing user settings. The merge only runs for the claude target — Cursor and other targets are unaffected.

Root cause

  • hooks-runtime module paths (hooks, scripts/hooks, scripts/lib) are copied to ~/.claude/ as-is
  • The generic Claude adapter (claude-home.js) has no custom planOperations() to handle hooks specially
  • Unlike Cursor (which reads a standalone hooks.json), Claude Code requires hooks in settings.json

Changes

  • scripts/lib/install/apply.js: Added mergeHooksIntoSettings(plan) — after file copies complete, reads ~/.claude/hooks/hooks.json, extracts the hooks object, and merges it into ~/.claude/settings.json (preserving existing settings)
  • tests/scripts/install-apply.test.js: Added 3 test cases — hooks merge for claude target, preserving existing settings, and skipping for non-claude targets

Type

  • Bug fix (installer)

Testing

  • All 15 install-apply tests pass (12 existing + 3 new)
  • Verified with real --profile full install on Windows
  • Confirmed hooks appear in settings.json after install

Checklist

  • Follows format guidelines
  • Tested with Claude Code
  • No sensitive info (API keys, paths)
  • Clear descriptions

Summary by cubic

Fixes inactive hooks for the claude target by merging hooks from ~/.claude/hooks/hooks.json into ~/.claude/settings.json after install. Preserves user-defined hook event types on reinstall; other targets are unaffected.

  • Bug Fixes
    • Added post-install per-event merge in applyInstallPlan() to write hooks into settings.json; preserves existing fields and user-defined hook types, creates settings.json if missing, and warns to stderr on JSON parse errors.
    • Expanded tests to cover per-event merge, settings preservation with pre-existing hooks, and non-claude skip.

Written for commit 46329ae. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Hooks are now automatically merged into application settings when installing to the Claude target.
    • Existing top-level settings are preserved when hooks are integrated; merge does not overwrite existing hook entries.
  • Bug Fixes

    • Merge step is skipped for non-Claude targets (no settings file created).
  • Tests

    • Added tests validating hooks integration behavior across targets and preserving existing settings.

The installer copies hooks/hooks.json to ~/.claude/hooks/hooks.json,
but Claude Code only reads hooks from ~/.claude/settings.json. This
causes all 28 hooks to be silently inactive after installation.

Add a post-install merge step in applyInstallPlan() that writes the
hooks field from hooks.json into settings.json, preserving any
existing user settings. The merge only runs for the claude target.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcb55b68-27e0-417b-8a6c-49f60bb3489c

📥 Commits

Reviewing files that changed from the base of the PR and between 49eae48 and 46329ae.

📒 Files selected for processing (2)
  • scripts/lib/install/apply.js
  • tests/scripts/install-apply.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/scripts/install-apply.test.js
  • scripts/lib/install/apply.js

📝 Walkthrough

Walkthrough

The PR updates applyInstallPlan to use Node's path.dirname when creating parent directories for copied files and adds a mergeHooksIntoSettings(plan) step that, for plan.adapter.target === 'claude', reads hooks/hooks.json, validates and merges its hooks object into settings.json (creating/reading/writing with defensive JSON error handling and directory creation). Tests for Claude and non-Claude targets were added.

Changes

Cohort / File(s) Summary
Install apply logic
scripts/lib/install/apply.js
Import path, use path.dirname for parent directory creation; add mergeHooksIntoSettings(plan) invoked after file copy operations. For Claude target, reads hooks/hooks.json, validates hooks is an object, reads existing settings.json (falls back on parse error), merges settings.hooks = {...settings.hooks, ...hooksConfig.hooks}, ensures parent dirs, writes settings.json, and logs parse warnings to stderr.
Integration tests
tests/scripts/install-apply.test.js
Add three tests: (1) Claude target merges hooks into a fresh settings.json and creates PreToolUse, (2) Claude merge preserves existing top-level fields and existing hook entries while adding new hook keys, (3) Cursor (non-Claude) target skips merge and does not create target settings.json.

Sequence Diagram(s)

sequenceDiagram
  actor CLI
  participant Installer as Installer (applyInstallPlan)
  participant FS as FileSystem
  participant Settings as settings.json
  participant Hooks as hooks/hooks.json

  CLI->>Installer: run install plan
  Installer->>FS: copy plan.operations files (ensure parent dirs via path.dirname)
  Installer->>Hooks: read hooks/hooks.json (if target === "claude")
  alt hooks file exists and parses
    Hooks-->>Installer: hooksConfig.hooks
    Installer->>Settings: read existing settings.json (if exists)
    alt settings parsed
      Settings-->>Installer: current settings object
    else parse error or missing
      Settings-->>Installer: {} (fallback)
    end
    Installer->>Settings: write merged settings (ensure parent dir)
  else missing hooks file
    Hooks-->>Installer: no-op (return)
  end
  Installer->>CLI: finish
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 I hopped through files, a tiny sleuth,
Merged hooks with care and gentle proof,
For Claude I stitched the settings bright,
Preserving fields and setting right,
A rabbit's cheer for code and light.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(hooks): merge hooks config into settings.json for claude target' directly and accurately summarizes the main change: merging hooks configuration into settings.json specifically for the claude target.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR fixes a real bug where all 28 ECC hooks were silently inactive after installation for the claude target because Claude Code reads hooks from ~/.claude/settings.json and not from a standalone hooks/hooks.json. The fix adds a mergeHooksIntoSettings() step in applyInstallPlan() that runs after the file-copy phase, reads the freshly-installed hooks/hooks.json, and shallow-merges its hooks object into settings.json.\n\nKey observations:\n- The core logic is sound: target guard, existence check, parse-error warnings to stderr, and a { ...settings.hooks, ...hooksConfig.hooks } shallow merge that preserves user-defined event types not defined by ECC (e.g. UserPromptSubmit).\n- A previously-flagged concern remains open: because ECC hook keys are spread last, any user-customised entries for event types that ECC also defines (e.g. PreToolUse) are silently replaced on every reinstall.\n- Two minor P2 findings: the typeof guard does not exclude arrays, and the $schema field from hooks.json is not forwarded to a freshly-created settings.json.

Confidence Score: 4/5

Safe to merge for most users, but users with pre-existing ECC-defined hook event types in settings.json will have those entries silently replaced on every reinstall.

The bug fix is correct and tests pass. The two new findings are P2. However, the previously-flagged P1 shallow-merge issue — user-defined hooks for event types shared with ECC (e.g. PreToolUse) are overwritten on reinstall with no warning — remains unaddressed, keeping the score at 4.

scripts/lib/install/apply.js — the merge strategy on line 46 and the Array guard on line 31.

Important Files Changed

Filename Overview
scripts/lib/install/apply.js Adds mergeHooksIntoSettings() post-copy step that reads hooks/hooks.json and shallow-merges its hooks object into settings.json; correctly guarded behind the claude target check and includes parse-error warnings.
tests/scripts/install-apply.test.js Three new integration tests cover first-install merge, non-hooks-field preservation, and cursor skip; the preservation test only validates non-ECC event types (UserPromptSubmit) so per-event-type overwrite for shared keys (e.g. PreToolUse) is not exercised.

Sequence Diagram

sequenceDiagram
    participant CLI as install-apply.js
    participant Apply as applyInstallPlan()
    participant FS as File System
    participant Merge as mergeHooksIntoSettings()

    CLI->>Apply: applyInstallPlan(plan)
    loop For each operation
        Apply->>FS: mkdirSync(dest dir)
        Apply->>FS: copyFileSync(src → dest)
    end
    Apply->>Merge: mergeHooksIntoSettings(plan)
    Merge->>Merge: check plan.adapter.target === 'claude'
    Merge->>FS: existsSync(hooks/hooks.json)
    FS-->>Merge: true
    Merge->>FS: readFileSync(hooks.json)
    FS-->>Merge: hooksConfig
    Merge->>FS: existsSync(settings.json)
    alt settings.json exists
        Merge->>FS: readFileSync(settings.json)
        FS-->>Merge: existing settings
    end
    Merge->>Merge: settings.hooks = { ...settings.hooks, ...hooksConfig.hooks }
    Merge->>FS: writeFileSync(settings.json)
    Apply->>FS: writeInstallState()
    Apply-->>CLI: { ...plan, applied: true }
Loading

Reviews (2): Last reviewed commit: "fix: merge hooks per event type and warn..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@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: 2

🧹 Nitpick comments (1)
tests/scripts/install-apply.test.js (1)

354-374: Strengthen preservation test to include pre-existing settings.hooks.

Right now this only proves non-hook fields survive. It won’t catch regressions where existing user hook groups are overwritten during merge.

✅ Suggested test enhancement
       fs.writeFileSync(
         path.join(claudeRoot, 'settings.json'),
-        JSON.stringify({ effortLevel: 'high', env: { MY_VAR: '1' } }, null, 2)
+        JSON.stringify({
+          effortLevel: 'high',
+          env: { MY_VAR: '1' },
+          hooks: {
+            UserPromptSubmit: [{ matcher: '*', hooks: [{ type: 'command', command: 'echo custom' }] }],
+          },
+        }, null, 2)
       );
@@
       assert.strictEqual(settings.effortLevel, 'high', 'existing effortLevel should be preserved');
       assert.deepStrictEqual(settings.env, { MY_VAR: '1' }, 'existing env should be preserved');
       assert.ok(settings.hooks, 'hooks should be merged in');
       assert.ok(settings.hooks.PreToolUse, 'PreToolUse hooks should exist');
+      assert.ok(settings.hooks.UserPromptSubmit, 'existing hooks should be preserved');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/scripts/install-apply.test.js` around lines 354 - 374, Update the test
"preserves existing settings.json fields when merging hooks" to seed
settings.json with an existing hooks object (e.g., include a hooks.PreToolUse
entry) before running run([...]) so the test verifies merging rather than only
non-hook fields; specifically, in the setup that writes to path.join(claudeRoot,
'settings.json') (inside the test function using homeDir/claudeRoot variables)
include a hooks property with at least one group and entry, then after run()
assert that those pre-existing hook groups/entries still exist and that new
hooks were merged (e.g., check settings.hooks.PreToolUse contains both the
original entry and any new entries and that no entire hook group was
overwritten).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/lib/install/apply.js`:
- Around line 24-28: The code currently swallows JSON.parse failures when
reading hooksJsonPath (assigning hooksConfig), which can hide invalid installs;
update the catch blocks around JSON.parse(fs.readFileSync(hooksJsonPath,
'utf8')) so they do not silently return—either rethrow a new Error with
contextual information (including hooksJsonPath and the original error.message)
or call the module's logger to log a clear failure before exiting; ensure the
same change is applied to the other try/catch that reads/parses hooksJsonPath so
invalid JSON fails fast with a descriptive message rather than silently
continuing.
- Around line 44-47: Current code overwrites settings.hooks with
hooksConfig.hooks which can drop existing user hook groups; instead merge
immutably by creating a new hooks object that combines existing settings.hooks
and hooksConfig.hooks (preserving existing entries and only adding/updating
incoming ones) and assign that to settings.hooks before serializing; refer to
settings.hooks and hooksConfig.hooks in your change and ensure you do not mutate
the original settings object (create a new object/clone for hooks), then write
the merged settings to settingsPath with fs.writeFileSync as before.

---

Nitpick comments:
In `@tests/scripts/install-apply.test.js`:
- Around line 354-374: Update the test "preserves existing settings.json fields
when merging hooks" to seed settings.json with an existing hooks object (e.g.,
include a hooks.PreToolUse entry) before running run([...]) so the test verifies
merging rather than only non-hook fields; specifically, in the setup that writes
to path.join(claudeRoot, 'settings.json') (inside the test function using
homeDir/claudeRoot variables) include a hooks property with at least one group
and entry, then after run() assert that those pre-existing hook groups/entries
still exist and that new hooks were merged (e.g., check
settings.hooks.PreToolUse contains both the original entry and any new entries
and that no entire hook group was overwritten).
🪄 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: d13c959c-bc1f-4299-b43c-a6ee24f745d5

📥 Commits

Reviewing files that changed from the base of the PR and between cc60bf6 and 49eae48.

📒 Files selected for processing (2)
  • scripts/lib/install/apply.js
  • tests/scripts/install-apply.test.js

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/lib/install/apply.js">

<violation number="1" location="scripts/lib/install/apply.js:40">
P1: Malformed `settings.json` is silently replaced with `{}` + hooks, risking user setting loss; parse failures are swallowed instead of failing install.</violation>

<violation number="2" location="scripts/lib/install/apply.js:44">
P1: Directly assigning `settings.hooks` replaces the entire existing hooks configuration and can delete user-defined hooks on reinstall. Merge with the current `settings.hooks` object instead of overwriting it wholesale.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Address review feedback from CodeRabbit, Greptile, and Cubic:

- Use spread merge ({ ...existing, ...incoming }) instead of full
  replacement so user-defined hook event types (e.g. UserPromptSubmit)
  survive reinstallation. ECC-managed event types still update.
- Log warnings to stderr when hooks.json or settings.json contain
  invalid JSON, instead of silently swallowing parse errors.
- Strengthen test to seed settings.json with a pre-existing hooks
  entry and verify it is preserved after install.
Copy link
Copy Markdown
Owner

Superseded by #964. #961 correctly identifies the Claude installer gap, but its current merge logic still overwrites existing hook arrays for shared events like PreToolUse. The maintainer-owned follow-up preserves existing event entries, keeps reinstall idempotent, and fails cleanly on malformed settings.json instead of replacing it.

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.

2 participants