Skip to content

fix(installer): preserve existing Claude hook settings#964

Open
affaan-m wants to merge 2 commits intomainfrom
fix/claude-hooks-settings-merge-safe
Open

fix(installer): preserve existing Claude hook settings#964
affaan-m wants to merge 2 commits intomainfrom
fix/claude-hooks-settings-merge-safe

Conversation

@affaan-m
Copy link
Copy Markdown
Owner

@affaan-m affaan-m commented Mar 27, 2026

Summary

  • merge hooks/hooks.json into ~/.claude/settings.json for Claude installs without overwriting existing hook entries in the same event group
  • keep the merge idempotent so reinstalling does not duplicate managed ECC hook entries
  • fail fast instead of replacing malformed settings.json with a fresh file

Why

#961 identifies the real installer gap, but its current merge logic still overwrites existing arrays for shared hook events like PreToolUse. That makes the branch unsafe as a final merge target for users who already have custom Claude hooks.

This PR supersedes #961 with a maintainer-owned branch that preserves existing event entries and adds explicit regression coverage for reinstall/idempotency and malformed settings handling.

Verification

  • NODE_PATH=/Users/affoon/Documents/GitHub/ECC/everything-claude-code/node_modules node tests/scripts/install-apply.test.js
  • NODE_PATH=/Users/affoon/Documents/GitHub/ECC/everything-claude-code/node_modules node tests/lib/install-request.test.js

Supersedes

  • #961

Summary by cubic

Preserves existing Claude hook settings during install by merging hooks into settings.json without overwriting user-defined entries. Reinstalls are idempotent and reject settings files whose root is not a JSON object.

  • Bug Fixes
    • Merge hooks by event without overwriting or duplicating entries.
    • Preserve existing settings fields and user-defined hook events.
    • Fail fast on malformed settings and non-object roots; leave file unchanged.
    • Add tests for merge behavior, reinstall idempotency, and invalid settings roots.

Written for commit 971cc3c. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Installer now manages hook configurations for Claude installs, merging new hook entries into existing settings and preventing duplicates.
    • Improved error handling: installer detects and reports malformed or invalid settings files and aborts safely without altering them.
  • Tests

    • Added thorough tests for hook merging, deduplication, merging with existing unrelated data, idempotency, and malformed/invalid settings handling.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

affaan-m has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

Installer now reads hooks/hooks.json for installs targeting plan.adapter.target === 'claude', validates and merges hook entries into ~/.claude/settings.json with per-event deduplication, creates directories as needed, and errors explicitly on malformed JSON in hooks or existing settings.

Changes

Cohort / File(s) Summary
Installer logic
scripts/lib/install/apply.js
Added path import; new mergeHooksIntoSettings(plan) and mergeHookEntries to load/validate hooks/hooks.json, load and validate existing settings.json, merge incoming hooks into settings.hooks with per-event deduplication, ensure directories, write updated settings.json, and throw on malformed JSON.
Tests
tests/scripts/install-apply.test.js
Added tests covering initial hook creation, merging with existing settings while preserving unrelated fields and user hooks, idempotency (no duplicate managed hooks on reinstall), and error handling for malformed or non-object settings.json.

Sequence Diagram

sequenceDiagram
    participant Installer as Install Script
    participant HooksFile as hooks/hooks.json
    participant SettingsFile as ~/.claude/settings.json
    participant FileSystem as File System

    Installer->>HooksFile: Read hooks/hooks.json
    activate HooksFile
    HooksFile-->>Installer: Return hooks JSON (or error)
    deactivate HooksFile

    Installer->>Installer: Validate hooks shape
    alt plan.adapter.target === "claude"
        Installer->>SettingsFile: Read existing settings.json (if exists)
        activate SettingsFile
        SettingsFile-->>Installer: Return settings JSON (or missing/error)
        deactivate SettingsFile

        Installer->>Installer: Validate settings is object
        Installer->>Installer: Merge incoming hooks into settings.hooks with per-event dedupe
        Installer->>FileSystem: Ensure settings directory exists
        activate FileSystem
        FileSystem-->>Installer: Directory ready
        deactivate FileSystem

        Installer->>SettingsFile: Write merged settings.json
        activate SettingsFile
        SettingsFile-->>Installer: Write complete
        deactivate SettingsFile
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 In a burrow of bytes I hop and sing,

Merging hooks so settings take wing,
No duplicates left in the dew,
JSON parsed clean, directories new,
Hooray — installs hum a brighter tune! 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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(installer): preserve existing Claude hook settings' directly and clearly summarizes the main change—improving the installer to preserve existing hook settings instead of overwriting them.

✏️ 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-hooks-settings-merge-safe

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
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.

1 issue 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:51">
P2: Validate `settings.json` root type after parsing. Non-object JSON currently gets merged and rewritten instead of failing fast.</violation>
</file>

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

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

affaan-m has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

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

🤖 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 77-84: The install currently mutates the target files before
validating settings which can leave a partial install; in applyInstallPlan, call
mergeHooksIntoSettings(plan) (or a new validateAndMergeSettings function) before
iterating plan.operations and performing fs.copyFileSync, so the Claude settings
are parsed/validated and any errors surface before touching files;
alternatively, stage writes by preparing temporary paths for each operation and
only perform the fs.mkdirSync/fs.copyFileSync loop after mergeHooksIntoSettings
succeeds, and ensure writeInstallState(plan.installStatePath, ...) still runs
only after successful copies.
- Around line 35-45: The parsed JSON root (hooksConfig) must be validated before
reading hooksConfig.hooks; update the parsing block to reject non-object roots
(null, array, or primitives) with a clear Error describing the file path and
expected shape, and then validate incomingHooks is an object (not array)
otherwise throw a descriptive installer error; apply the same pattern for the
analogous settings parsing (e.g., settingsConfig and incomingSettings) so both
code paths fail fast with explicit messages rather than silently returning or
throwing raw TypeErrors.
🪄 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: 97bc3558-8ee7-49b3-9cb1-5b4651ab3d5a

📥 Commits

Reviewing files that changed from the base of the PR and between 8b6140d and bb5d106.

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

Comment on lines +35 to +45
let hooksConfig;
try {
hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
} catch (error) {
throw new Error(`Failed to parse hooks config at ${hooksJsonPath}: ${error.message}`);
}

const incomingHooks = hooksConfig.hooks;
if (!incomingHooks || typeof incomingHooks !== 'object' || Array.isArray(incomingHooks)) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the parsed JSON root before touching .hooks.

JSON.parse() accepts null, arrays, and primitives. If hooks/hooks.json is null, Line 42 throws a raw TypeError; if settings.json is null, Line 57 does the same. Lines 43-45 also turn an invalid top-level hooks payload into a silent success. Reject non-object roots and invalid hooks shapes explicitly so this path always fails with a clear installer error.

🛡️ Proposed fix
   try {
     hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
   } catch (error) {
     throw new Error(`Failed to parse hooks config at ${hooksJsonPath}: ${error.message}`);
   }
+  if (!hooksConfig || typeof hooksConfig !== 'object' || Array.isArray(hooksConfig)) {
+    throw new Error(`Invalid hooks config at ${hooksJsonPath}: expected a JSON object`);
+  }

   const incomingHooks = hooksConfig.hooks;
   if (!incomingHooks || typeof incomingHooks !== 'object' || Array.isArray(incomingHooks)) {
-    return;
+    throw new Error(`Invalid hooks config at ${hooksJsonPath}: expected a top-level "hooks" object`);
   }

   const settingsPath = path.join(plan.targetRoot, 'settings.json');
   let settings = {};
   if (fs.existsSync(settingsPath)) {
     try {
       settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
     } catch (error) {
       throw new Error(`Failed to parse existing settings at ${settingsPath}: ${error.message}`);
     }
+    if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
+      throw new Error(`Invalid existing settings at ${settingsPath}: expected a JSON object`);
+    }
   }

As per coding guidelines, "Always validate all user input before processing at system boundaries", "Use schema-based validation where available", and "Fail fast with clear error messages when validation fails".

Also applies to: 49-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/lib/install/apply.js` around lines 35 - 45, The parsed JSON root
(hooksConfig) must be validated before reading hooksConfig.hooks; update the
parsing block to reject non-object roots (null, array, or primitives) with a
clear Error describing the file path and expected shape, and then validate
incomingHooks is an object (not array) otherwise throw a descriptive installer
error; apply the same pattern for the analogous settings parsing (e.g.,
settingsConfig and incomingSettings) so both code paths fail fast with explicit
messages rather than silently returning or throwing raw TypeErrors.

Copy link
Copy Markdown
Owner Author

Addressed the latest cubic finding in 971cc3c by rejecting non-object settings.json roots and adding regression coverage for that case.

Verification:

  • NODE_PATH=/Users/affoon/Documents/GitHub/ECC/everything-claude-code/node_modules node tests/scripts/install-apply.test.js
  • NODE_PATH=/Users/affoon/Documents/GitHub/ECC/everything-claude-code/node_modules node tests/lib/install-request.test.js

@cubic-dev-ai please re-review the updated head.

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai bot commented Mar 27, 2026

Addressed the latest cubic finding in 971cc3c by rejecting non-object settings.json roots and adding regression coverage for that case.

Verification:

  • NODE_PATH=/Users/affoon/Documents/GitHub/ECC/everything-claude-code/node_modules node tests/scripts/install-apply.test.js
  • NODE_PATH=/Users/affoon/Documents/GitHub/ECC/everything-claude-code/node_modules node tests/lib/install-request.test.js
    ...

@affaan-m I have started the AI code review. It will take a few minutes to complete.

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.

♻️ Duplicate comments (1)
scripts/lib/install/apply.js (1)

35-45: ⚠️ Potential issue | 🟠 Major

Missing validation of hooksConfig root before accessing .hooks.

The past review flagged this: if hooks/hooks.json contains null, an array, or a primitive, JSON.parse succeeds but hooksConfig.hooks (line 42) throws a raw TypeError instead of a clear installer error.

The fix was applied for settings.json (lines 52-54) but not here. Add the same guard after parsing hooksConfig.

🛡️ Proposed fix
   try {
     hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
   } catch (error) {
     throw new Error(`Failed to parse hooks config at ${hooksJsonPath}: ${error.message}`);
   }
+  if (!hooksConfig || typeof hooksConfig !== 'object' || Array.isArray(hooksConfig)) {
+    throw new Error(`Invalid hooks config at ${hooksJsonPath}: expected a JSON object`);
+  }

   const incomingHooks = hooksConfig.hooks;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/lib/install/apply.js` around lines 35 - 45, After parsing hooksConfig
from hooksJsonPath, validate that hooksConfig is a plain object before accessing
.hooks (same guard used for settings.json): if hooksConfig is falsy, not an
object, or an array, throw a clear Error mentioning hooksJsonPath; then proceed
to read const incomingHooks = hooksConfig.hooks and keep the existing checks for
incomingHooks. Update the code around the hooksConfig parse (symbols:
hooksConfig, hooksJsonPath, incomingHooks) to perform that guard and throw a
descriptive installer error instead of allowing a raw TypeError.
🧹 Nitpick comments (1)
scripts/lib/install/apply.js (1)

8-23: JSON.stringify key ordering may cause deduplication misses.

JSON.stringify produces different strings for semantically identical objects when property ordering differs (e.g., {a:1, b:2} vs {b:2, a:1}). If hooks are created by different tools or manually edited, duplicates could slip through.

Consider normalizing key order or using a deep-equals comparison if exact deduplication is required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/lib/install/apply.js` around lines 8 - 23, mergeHookEntries currently
deduplicates by JSON.stringify(entry) which can miss semantically identical
objects with different property orders; change the deduplication key to a stable
representation by normalizing object keys before stringifying (e.g., perform a
deterministic, recursive key sort or use a stableStringify helper) or replace
the Set lookup with a deep-equality check against mergedEntries; update
references inside mergeHookEntries (entryKey, seenEntries, mergedEntries) to use
the normalized key or deep-equal logic so objects with the same properties but
different key orders are treated as duplicates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@scripts/lib/install/apply.js`:
- Around line 35-45: After parsing hooksConfig from hooksJsonPath, validate that
hooksConfig is a plain object before accessing .hooks (same guard used for
settings.json): if hooksConfig is falsy, not an object, or an array, throw a
clear Error mentioning hooksJsonPath; then proceed to read const incomingHooks =
hooksConfig.hooks and keep the existing checks for incomingHooks. Update the
code around the hooksConfig parse (symbols: hooksConfig, hooksJsonPath,
incomingHooks) to perform that guard and throw a descriptive installer error
instead of allowing a raw TypeError.

---

Nitpick comments:
In `@scripts/lib/install/apply.js`:
- Around line 8-23: mergeHookEntries currently deduplicates by
JSON.stringify(entry) which can miss semantically identical objects with
different property orders; change the deduplication key to a stable
representation by normalizing object keys before stringifying (e.g., perform a
deterministic, recursive key sort or use a stableStringify helper) or replace
the Set lookup with a deep-equality check against mergedEntries; update
references inside mergeHookEntries (entryKey, seenEntries, mergedEntries) to use
the normalized key or deep-equal logic so objects with the same properties but
different key orders are treated as duplicates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2e7b0625-13e9-46cc-9466-6995153a9f60

📥 Commits

Reviewing files that changed from the base of the PR and between bb5d106 and 971cc3c.

📒 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 (1)
  • 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.

1 issue 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:42">
P2: Validate the parsed hooks config root and `hooks` shape before dereferencing; otherwise non-object JSON roots can throw a runtime `TypeError`, and invalid `hooks` payloads are silently ignored so installation appears successful without applying hooks.</violation>
</file>

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

Comment on lines +42 to +45
const incomingHooks = hooksConfig.hooks;
if (!incomingHooks || typeof incomingHooks !== 'object' || Array.isArray(incomingHooks)) {
return;
}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

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

P2: Validate the parsed hooks config root and hooks shape before dereferencing; otherwise non-object JSON roots can throw a runtime TypeError, and invalid hooks payloads are silently ignored so installation appears successful without applying hooks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/lib/install/apply.js, line 42:

<comment>Validate the parsed hooks config root and `hooks` shape before dereferencing; otherwise non-object JSON roots can throw a runtime `TypeError`, and invalid `hooks` payloads are silently ignored so installation appears successful without applying hooks.</comment>

<file context>
@@ -1,15 +1,89 @@
+    throw new Error(`Failed to parse hooks config at ${hooksJsonPath}: ${error.message}`);
+  }
+
+  const incomingHooks = hooksConfig.hooks;
+  if (!incomingHooks || typeof incomingHooks !== 'object' || Array.isArray(incomingHooks)) {
+    return;
</file context>
Suggested change
const incomingHooks = hooksConfig.hooks;
if (!incomingHooks || typeof incomingHooks !== 'object' || Array.isArray(incomingHooks)) {
return;
}
if (!hooksConfig || typeof hooksConfig !== 'object' || Array.isArray(hooksConfig)) {
throw new Error(`Invalid hooks config at ${hooksJsonPath}: expected a JSON object`);
}
const incomingHooks = hooksConfig.hooks;
if (!incomingHooks || typeof incomingHooks !== 'object' || Array.isArray(incomingHooks)) {
throw new Error(`Invalid hooks config at ${hooksJsonPath}: expected a top-level "hooks" object`);
}
Fix with Cubic

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.

1 participant