Skip to content

fix: isolate hooks as CommonJS so they survive ESM parent package.json#174

Merged
JuliusBrussee merged 1 commit intoJuliusBrussee:mainfrom
malakhov-dmitrii:fix/commonjs-override-hooks
Apr 15, 2026
Merged

fix: isolate hooks as CommonJS so they survive ESM parent package.json#174
JuliusBrussee merged 1 commit intoJuliusBrussee:mainfrom
malakhov-dmitrii:fix/commonjs-override-hooks

Conversation

@malakhov-dmitrii
Copy link
Copy Markdown
Contributor

@malakhov-dmitrii malakhov-dmitrii commented Apr 15, 2026

What

Adds hooks/package.json with { "type": "commonjs" } so the caveman hooks resolve as CJS regardless of what the user's ~/.claude/package.json declares. Also registers the new file in install.{sh,ps1} and uninstall.{sh,ps1} so standalone installs (curl | bash / PowerShell) copy it into ~/.claude/hooks/.

Why

When ~/.claude/package.json (or any ancestor) contains "type": "module", Node treats every .js file under that tree as an ES module. The caveman hooks are CommonJS (require('fs')), so they crash immediately:

ReferenceError: require is not defined in ES module scope
    at file:///.../hooks/caveman-activate.js:9:12

surfaced to the user as:

SessionStart:clear hook error
Failed with non-blocking status code: file:///.../caveman-activate.js:9
UserPromptSubmit hook error
Failed with non-blocking status code: file:///.../caveman-mode-tracker.js:5

Repros on any user who has { "type": "module" } in ~/.claude/package.json β€” which several Claude Code plugins/skills set up. On my machine this stopped both hooks from firing (flag file not written, ruleset never injected, statusline badge missing).

The ESM sub-case is flagged in @mrx-arafat's comment on #167 β€” recommending exactly this workaround but applied by the user, not shipped by the plugin. Shipping it here means the fix survives plugin updates and reaches every install without user action.

How the fix works

Node's module-type resolution walks up looking for the nearest package.json. By dropping one into hooks/ itself, we pin the directory to CJS regardless of any ancestor settings. Zero code changes to the hooks themselves.

Resolution order:

  • Before: ~/.claude/package.json β†’ "type": "module" β†’ .js treated as ESM β†’ require undefined β†’ crash.
  • After: hooks/package.json (nearer ancestor) β†’ "type": "commonjs" β†’ .js treated as CJS β†’ hook runs.

Scope note

This does not fix the Windows ${CLAUDE_PLUGIN_ROOT} path-expansion issues with spaces/umlauts in #167 / #78 / #72 β€” those have a separate root cause (unquoted variable in plugin.json) and need their own patch.

Verification

Simulated ESM-parent environment:

mkdir -p /tmp/esm-test/.claude/plugins/hooks
echo '{"type":"module"}' > /tmp/esm-test/.claude/package.json
cp hooks/{package.json,caveman-config.js,caveman-activate.js,caveman-mode-tracker.js} \
   /tmp/esm-test/.claude/plugins/hooks/

HOME=/tmp/esm-test node /tmp/esm-test/.claude/plugins/hooks/caveman-activate.js
# β†’ exit 0, emits "CAVEMAN MODE ACTIVE β€” level: full" + ruleset

echo '{"prompt":"hi"}' | HOME=/tmp/esm-test \
  node /tmp/esm-test/.claude/plugins/hooks/caveman-mode-tracker.js
# β†’ exit 0

Without the fix both commands fail with ReferenceError: require is not defined in ES module scope.

Files changed

  • hooks/package.json (new) β€” { "type": "commonjs" }
  • hooks/install.sh β€” add package.json to HOOK_FILES
  • hooks/install.ps1 β€” add package.json to $HookFiles
  • hooks/uninstall.sh β€” add package.json to HOOK_FILES
  • hooks/uninstall.ps1 β€” add package.json to $HookFiles

When ~/.claude/package.json (or any ancestor) contains "type": "module",
Node treats every .js file under that tree as an ES module. The caveman
hooks use require() and crash with:

  ReferenceError: require is not defined in ES module scope

surfaced as:

  SessionStart:clear hook error / UserPromptSubmit hook error
  Failed with non-blocking status code: .../caveman-activate.js:9

This pins the hooks directory to CommonJS via a local package.json, so
module resolution no longer depends on whatever the user's ~/.claude
directory declares. Also wires the new file into install/uninstall
scripts so standalone installs (curl | bash / Invoke-WebRequest)
copy it into ~/.claude/hooks/ alongside the JS files.

Addresses the ESM sub-case flagged in JuliusBrussee#167 (comment by mrx-arafat).
Does not fix the Windows path-with-spaces expansion issues in JuliusBrussee#167/JuliusBrussee#78/JuliusBrussee#72
which have a separate root cause in plugin.json ${CLAUDE_PLUGIN_ROOT}
quoting.
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