Skip to content

fix(plugins): use mergeHooksSettings() for marketplace hook supplement#1056

Closed
rodrigoeqnit wants to merge 6 commits into
Gitlawb:mainfrom
rodrigoeqnit:fix/pluginloader-hooks-merge
Closed

fix(plugins): use mergeHooksSettings() for marketplace hook supplement#1056
rodrigoeqnit wants to merge 6 commits into
Gitlawb:mainfrom
rodrigoeqnit:fix/pluginloader-hooks-merge

Conversation

@rodrigoeqnit
Copy link
Copy Markdown
Contributor

Fixes #1055

Summary

  • In finishLoadingPluginFromPath, the marketplace hook supplement path used an object spread that overwrote per-event matcher arrays from plugin.json with those from the marketplace entry
  • mergeHooksSettings() already exists in the same file and concatenates arrays correctly — it was used in createPluginFromPath but not in this supplement path
  • This fix replaces the spread with mergeHooksSettings() (3-line change)

What changed

src/utils/plugins/pluginLoader.ts — supplement hooks path in finishLoadingPluginFromPath:

-    if (entry.hooks) {
-      plugin.hooksConfig = {
-        ...(plugin.hooksConfig || {}),
-        ...(entry.hooks as HooksSettings),
-      }
-    }
+    // CORRECTNESS: Use mergeHooksSettings() instead of object spread so that
+    // per-event matcher arrays from plugin.json and the marketplace entry are
+    // concatenated rather than the marketplace entry silently overwriting the
+    // plugin.json matchers for the same event.
+    if (entry.hooks) {
+      plugin.hooksConfig = mergeHooksSettings(
+        plugin.hooksConfig,
+        entry.hooks as HooksSettings,
+      )
+    }

Test plan

  • Plugin with plugin.json hooks + marketplace supplement for same event → both matchers present after load
  • Plugin with only plugin.json hooks, no marketplace supplement → no change in behavior
  • Plugin with only marketplace entry hooks, no plugin.jsonmergeHooksSettings(undefined, hooks) returns hooks (identical to old assignment)

Copy link
Copy Markdown
Collaborator

@jatmn jatmn left a comment

Choose a reason for hiding this comment

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

Thanks for the fix. I validated the claim against the current loader path and agree with the root cause: in the hasManifest && strict marketplace supplement path, the old object spread replaced same-event hook arrays, while mergeHooksSettings() preserves the plugin manifest entries and appends the marketplace entries.

Please fix one thing before merge:

Missing Regression Coverage

src/utils/plugins/pluginLoader.ts:2919

The implementation change is correct, but this bug is exactly the kind of loader behavior that can regress silently. Please add a focused test that loads a marketplace plugin with:

  • plugin.json declaring a hook for an event such as PreToolUse
  • the marketplace entry supplementing hooks for that same event
  • an assertion that the final hooksConfig.PreToolUse contains both matchers in order

I checked the current focused test file, src/utils/plugins/pluginLoader.test.ts, and it only covers mergePluginSources; it does not exercise finishLoadingPluginFromPath, marketplace supplementation, or hook merging.

Validation Performed

  • Reviewed PR delta: one-file change in src/utils/plugins/pluginLoader.ts
  • Checked linked issue #1055 and confirmed the PR addresses the described overwrite path
  • Ran bun test src/utils/plugins/pluginLoader.test.ts: passed, 2 tests
  • Ran bun run typecheck: currently fails on broad unrelated baseline errors across the repo, so it did not provide PR-specific signal

Copy link
Copy Markdown
Collaborator

@gnanam1990 gnanam1990 left a comment

Choose a reason for hiding this comment

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

Thanks — root cause and fix are correct: switching the marketplace supplement path from object spread to mergeHooksSettings() matches what createPluginFromPath already does, so per-event matcher arrays from plugin.json and the marketplace entry are concatenated instead of overwritten.

Blocking:

  1. Missing regression coverage in src/utils/plugins/pluginLoader.test.ts. The current suite only exercises mergePluginSources, so this fix is unprotected against regression in finishLoadingPluginFromPath. Please add a focused test that loads a marketplace plugin where plugin.json declares a hook for, e.g., PreToolUse and the marketplace entry supplements hooks.PreToolUse, asserting the merged config preserves both matchers in order.

Non-blocking:

  • The inline comment is good and explains the invariant clearly.
  • No CI runs are attached — once the test is added, CI will exercise the path.

Verified locally: read the diff, confirmed mergeHooksSettings() is already exported in the same file and used in createPluginFromPath. Did not run focused tests as no new test file exists yet.

Copy link
Copy Markdown
Collaborator

@Vasanthdev2004 Vasanthdev2004 left a comment

Choose a reason for hiding this comment

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

Targeted review of the current head (9826124).

Verdict: Needs changes

Blocking issue:

  1. The code change itself is the right fix, but this loader path still needs regression coverage. Please add a focused test where plugin.json defines a hook for the same event as the marketplace entry supplement, then assert the final hooksConfig contains both matcher arrays in order.

This is exactly the kind of plugin loader behavior that can silently regress, so I do not want to merge it untested even though the implementation direction is correct.

rodrigoeqnit added a commit to rodrigoeqnit/openclaude that referenced this pull request May 8, 2026
Object spread silently overwrites plugin.json matchers for the same event
when a marketplace entry also declares hooks for that event. Switch to
mergeHooksSettings() so per-event matcher arrays are concatenated instead.

Adds regression tests for mergeHooksSettings directly (exported for testing).

Closes Gitlawb#1056
@rodrigoeqnit rodrigoeqnit force-pushed the fix/pluginloader-hooks-merge branch from 9826124 to 33b447f Compare May 8, 2026 14:21
@rodrigoeqnit
Copy link
Copy Markdown
Contributor Author

Updated with the regression tests requested in review.

Changes:

  • src/utils/plugins/pluginLoader.tsmergeHooksSettings exported (needed for direct unit testing)
  • src/utils/plugins/pluginLoader.test.ts — 3 new tests for mergeHooksSettings: concatenates matchers for same event, returns additional when base is undefined, adds new event from additional

The test asserting that PreToolUse matchers from both plugin.json and the marketplace entry are preserved in order is included. Please re-review when you get a chance.

Copy link
Copy Markdown
Collaborator

@jatmn jatmn left a comment

Choose a reason for hiding this comment

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

Thanks for the update. The production fix still looks correct: using mergeHooksSettings() in the marketplace supplement path preserves the plugin.json hook matchers and appends marketplace hook matchers for the same event instead of overwriting them.

However, I do not think the requested change is complete yet.

Blocking

src/utils/plugins/pluginLoader.test.ts:22

The added coverage tests mergeHooksSettings() directly, but the requested regression test was for the loader behavior that actually regressed: loading a marketplace plugin where plugin.json defines, for example, hooks.PreToolUse, the marketplace entry also supplements hooks.PreToolUse, and the final loaded plugin has both matchers in order.

That distinction matters because the bug was not in the helper itself; it was that finishLoadingPluginFromPath() was not using the helper in the strict marketplace supplement path. A direct helper test can pass even if this loader path later regresses back to a spread or otherwise stops applying marketplace hook supplementation correctly.

Please add the focused loader-level regression test requested in the previous review. The direct mergeHooksSettings() unit tests are fine to keep, but they do not replace the integration coverage for finishLoadingPluginFromPath().

Validation

  • Reviewed PR head 33b447f1f694d52fd5100f1593cb647d4b4d397b.
  • Confirmed src/utils/plugins/pluginLoader.ts now uses mergeHooksSettings() in the marketplace hook supplement path.
  • Confirmed the added tests only exercise mergeHooksSettings() and mergePluginSources; they do not exercise finishLoadingPluginFromPath() or the final loaded hooksConfig.
  • Ran bun test src/utils/plugins/pluginLoader.test.ts: passed, 5 tests.
  • Ran bun run typecheck: failed on broad existing repo-wide errors outside this PR-specific area, so it did not provide PR-specific signal.

Verdict: changes requested. The implementation is good, but the requested regression coverage has not been completed yet.

Copy link
Copy Markdown
Collaborator

@gnanam1990 gnanam1990 left a comment

Choose a reason for hiding this comment

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

Thanks for the update — the production change is still the right fix, and the new direct tests for mergeHooksSettings() are a nice addition. Re-reviewing against the new head 33b447f since my prior changes-requested was on 9826124.

Still blocking

The requested regression test was specifically at the loader level — exercising finishLoadingPluginFromPath with a marketplace plugin where plugin.json and the marketplace entry both supplement hooks for the same event, and asserting the resulting hooksConfig.PreToolUse contains both matchers in order.

The helper-level tests on mergeHooksSettings() lock in the helper's contract, but they do not protect against the actual regression that landed this bug: the loader path not calling the helper. If someone reverts the supplement path back to an object spread (or replaces it with any other merge that drops matchers), the helper unit tests still pass and the bug recurs silently.

A focused integration test using a tmp-dir fixture (write a plugin.json + a small marketplace entry, call into the load path, assert on the merged hooksConfig) is the coverage we need. Aligning with @jatmn's earlier comment on this point.

Non-blocking

  • The inline comment in pluginLoader.ts is good — clearly documents the invariant.
  • Exporting mergeHooksSettings for direct testing is fine.

Once the loader-level test is in, this should be ready.

@rodrigoeqnit rodrigoeqnit force-pushed the fix/pluginloader-hooks-merge branch from 33b447f to 7ccff26 Compare May 9, 2026 17:30
rodrigoeqnit added a commit to rodrigoeqnit/openclaude that referenced this pull request May 9, 2026
…merge

Exports finishLoadingPluginFromPath so it can be tested directly.
Adds a tmp-dir integration test that writes a plugin.json + hooks/hooks.json
with a PreToolUse matcher, then runs finishLoadingPluginFromPath with a
marketplace entry that also supplements PreToolUse. Asserts that
hooksConfig.PreToolUse contains both matchers in order (plugin.json first,
marketplace second), exercising the mergeHooksSettings() call path that
was broken before this fix and could silently regress if reverted.

Addresses reviewer feedback on PR Gitlawb#1056.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rodrigoeqnit
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed feedback. The loader-level integration test is now in place.

What was added (head b2e4f7f):

  1. pluginLoader.ts: Exported finishLoadingPluginFromPath so the function is directly testable without going through the full marketplace download path.

  2. pluginLoader.test.ts: New finishLoadingPluginFromPath — marketplace hook supplement describe block with a tmp-dir integration test:

    • Writes a real .claude-plugin/plugin.json and hooks/hooks.json (with PreToolUse: Bash) to a temp directory.
    • Calls finishLoadingPluginFromPath with a marketplace entry that supplements PreToolUse with a second matcher (Write).
    • Asserts hooksConfig.PreToolUse has length 2, with Bash first and Write second.

If someone reverts the mergeHooksSettings call back to an object spread (or any other merge that drops matchers), this test fails — the loader path is now directly exercised.

All 6 tests pass: bun test src/utils/plugins/pluginLoader.test.ts.

Copy link
Copy Markdown
Collaborator

@jatmn jatmn left a comment

Choose a reason for hiding this comment

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

Changes requested.

The production change looks correct: the hasManifest && strict marketplace supplement path in finishLoadingPluginFromPath() now uses mergeHooksSettings() instead of object spread, so same-event hook matcher arrays from the plugin's own hook config and the marketplace entry are concatenated rather than overwritten.

Requested Fix

src/utils/plugins/pluginLoader.ts

Please fix the stale comment above mergeHooksSettings(). It currently says the helper is exported for tests of the marketplace supplement loader path and references createPluginFromPath:~2919, but the path under test is now finishLoadingPluginFromPath(). That comment should point at the actual loader function/path so future readers do not chase the wrong function while investigating this regression coverage.

My Prior Requested Changes

Completed.

Previous blocking request was to add loader-level regression coverage that:

  • loads a marketplace plugin with a plugin.json;
  • has the plugin's on-disk hook config define hooks.PreToolUse;
  • has the marketplace entry supplement hooks.PreToolUse;
  • asserts the final loaded hooksConfig.PreToolUse contains both matchers in order.

The current PR head includes this in src/utils/plugins/pluginLoader.test.ts under finishLoadingPluginFromPath - marketplace hook supplement. It creates a temp plugin directory, writes .claude-plugin/plugin.json plus hooks/hooks.json, calls finishLoadingPluginFromPath() with marketplace PreToolUse hooks, and asserts the loaded matchers are Bash then Write.

The earlier helper-only tests for mergeHooksSettings() are also present, but they are no longer the only coverage; the requested loader-level path is now exercised.

Review Notes

  • src/utils/plugins/pluginLoader.ts now exports mergeHooksSettings() and finishLoadingPluginFromPath() to make the regression tests possible. These are not package-level public exports, so the practical API surface impact is limited to internal module imports.
  • The merge behavior preserves existing order: plugin/on-disk hooks remain first, marketplace-supplemented hooks are appended.
  • The no-manifest marketplace-entry path still assigns entry.hooks directly, which is consistent with the previous behavior and not part of the reported overwrite bug.

Validation

  • bun install --frozen-lockfile: passed.
  • bun test src/utils/plugins/pluginLoader.test.ts: passed, 6 tests.
  • bun run typecheck: failed on broad existing repo-wide baseline errors. I did not see PR-specific errors in src/utils/plugins/pluginLoader.ts or src/utils/plugins/pluginLoader.test.ts.

@rodrigoeqnit
Copy link
Copy Markdown
Contributor Author

Stale comment fixed in b50014f.

The comment above mergeHooksSettings() referenced createPluginFromPath as the callsite, but the marketplace hook supplement block was extracted into finishLoadingPluginFromPath in a later refactor. Updated to point to the correct function.

Copy link
Copy Markdown
Collaborator

@jatmn jatmn left a comment

Choose a reason for hiding this comment

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

Findings

  1. src/utils/plugins/pluginLoader.test.ts:34 still says the affected loader path is createPluginFromPath, but the bug and regression path under test are in finishLoadingPluginFromPath. I would update that comment so the test documentation matches the implementation and prior review discussion.

Completion Of My Requested Fixes

I confirmed my requested regression coverage has been added.

  • src/utils/plugins/pluginLoader.test.ts:136 now includes a focused finishLoadingPluginFromPath test that loads a plugin with existing PreToolUse hooks and a marketplace supplement for the same event.
  • The test asserts both matchers are present in order, with the plugin hook first and the marketplace hook second.
  • The implementation change in src/utils/plugins/pluginLoader.ts:2916 now uses mergeHooksSettings(...) instead of object spread, which addresses the overwrite bug I called out.

@rodrigoeqnit
Copy link
Copy Markdown
Contributor Author

Addressed in commit a1073fd (pushed 2026-05-11):

The stale createPluginFromPath reference in the test comment at line 34 has been updated to finishLoadingPluginFromPath, matching the actual function under test and the prior review discussion.

@jatmn ready for re-review when you have a moment.

@jatmn
Copy link
Copy Markdown
Collaborator

jatmn commented May 11, 2026

Addressed in commit a1073fd (pushed 2026-05-11):

The stale createPluginFromPath reference in the test comment at line 34 has been updated to finishLoadingPluginFromPath, matching the actual function under test and the prior review discussion.

@jatmn ready for re-review when you have a moment.

please first clean the branch; it currently conflicts with main.
will gladly follow up with a review after

rodrigoeqnit and others added 6 commits May 14, 2026 09:51
When a plugin has both plugin.json and marketplace entry hooks for the
same event, the old object spread silently overwrote the plugin.json
matchers with the marketplace entry's matchers. mergeHooksSettings()
already exists in this file and correctly concatenates per-event matcher
arrays — it was used in createPluginFromPath but not in the marketplace
supplement path.

Affected path: finishLoadingPluginFromPath (has-manifest + strict mode)
…e path

The fix in the previous commit used mergeHooksSettings() instead of object
spread in the marketplace supplement path (createPluginFromPath:~2919).
These tests validate the key invariant: same-event hook arrays from
plugin.json and from the marketplace entry must be concatenated, not
overwritten.

Also exports mergeHooksSettings for direct testing as requested by reviewers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…merge

Exports finishLoadingPluginFromPath so it can be tested directly.
Adds a tmp-dir integration test that writes a plugin.json + hooks/hooks.json
with a PreToolUse matcher, then runs finishLoadingPluginFromPath with a
marketplace entry that also supplements PreToolUse. Asserts that
hooksConfig.PreToolUse contains both matchers in order (plugin.json first,
marketplace second), exercising the mergeHooksSettings() call path that
was broken before this fix and could silently regress if reverted.

Addresses reviewer feedback on PR Gitlawb#1056.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The explicit parameter annotation was narrower than the actual HooksSettings
union type, causing a TS2345 type error. Moved the cast to the array itself
so TypeScript can infer the callback parameter type correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ment

The comment referenced createPluginFromPath but the marketplace hook
supplement block lives in finishLoadingPluginFromPath (extracted later).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…omment

Matches the fix already applied to pluginLoader.ts: the loader path under
test is finishLoadingPluginFromPath, not createPluginFromPath.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rodrigoeqnit rodrigoeqnit force-pushed the fix/pluginloader-hooks-merge branch from a1073fd to 73d6d36 Compare May 14, 2026 12:56
@rodrigoeqnit
Copy link
Copy Markdown
Contributor Author

CI failure analysis

The 6 failing tests are not caused by changes in this PR. Same pre-existing test isolation issue also seen in #1059 (opened simultaneously).

All 4 providerProfile failures and the sdk-context-isolation failure share a common CLAUDE_CONFIG_DIR leak from the OpenClaude paths test suite:

Expected: '/tmp/openclaude-config-profile-.../config/.openclaude-profile.json'
Received: '/tmp/openclaude-paths-test-.../.openclaude/.openclaude-profile.json'

A test using prefix openclaude-paths-test- sets CLAUDE_CONFIG_DIR without restoring it, causing state to bleed into parallel test workers. The KnowledgeGraph failure is an unrelated flake.

These failures are reproducible on any branch that was rebased onto today's main — they don't exist in this PR's code changes.

@rodrigoeqnit
Copy link
Copy Markdown
Contributor Author

Update (2026-05-14): Same 6 tests failing again on today's CI run — and also on PR #1059 and PR #1076 (three different branches, same list). This is a confirmed repo-wide test suite issue unrelated to any branch content:

  • saveProfileFile defaults to user config instead of the working directory
  • loadProfileFile keeps project-local files as a legacy fallback
  • loadProfileFile does not fall back when user config profile is invalid
  • clearPersistedCodexOAuthProfile clears both default and legacy OAuth profiles
  • KnowledgeGraph Phase 1 Stress & Edge Cases > recovers from corrupted Orama file
  • SDK context isolation > regenerateSessionId > updates SDK context inside runWithSdkContext

2538 other tests pass. The PR changes (src/utils/pluginLoader.ts + mergeHooksSettings()) are unaffected.

@jatmn
Copy link
Copy Markdown
Collaborator

jatmn commented May 15, 2026

please update with main and try again , smoke issue should be resolved.

@rodrigoeqnit
Copy link
Copy Markdown
Contributor Author

@jatmn the stale comment (pointing to a non-existent line) was removed in commit 73d6d36. The PR is ready for another look when you have a moment.

@Vasanthdev2004
Copy link
Copy Markdown
Collaborator

Blockers

This is a duplicate of #1167 which has already been approved and merged.

Looks Good


Verdict: Close as duplicate#1167 already covers this fix.

@Vasanthdev2004
Copy link
Copy Markdown
Collaborator

Duplicate of #1167.

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.

fix(plugins): marketplace hook supplement drops plugin.json matchers (missing mergeHooksSettings)

4 participants