Describe the bug
When a Claude hook package places a JavaScript hook and its module metadata in a nested directory, APM deploys the JavaScript file but drops the nested package.json. APM then generates a CommonJS sidecar at the higher hook root, so Node executes the nested .js file with the wrong module semantics.
To Reproduce
Steps to reproduce the behavior:
-
Create a Claude hook package with this layout:
hooks/
hooks.json
esm/
entry.js
package.json
-
Set hooks/hooks.json to:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node ./esm/entry.js"
}
]
}
]
}
}
-
Set hooks/esm/entry.js to an ES module, for example:
import { writeFileSync } from "node:fs";
writeFileSync(process.argv[2], "ok\\n");
-
Set hooks/esm/package.json to:
-
Install the package into a project with Claude hooks enabled by running apm install.
-
Inspect the deployed hook under .claude/hooks/<package>/hooks/esm/.
-
Observe that entry.js is present, but esm/package.json is missing. The generated .claude/hooks/<package>/hooks/package.json contains {"type": "commonjs"}.
-
Run the deployed hook with Node and observe an import syntax error because the file is treated as CommonJS.
Expected behavior
APM should preserve the nested hooks/esm/package.json, or generate equivalent module metadata at the deployed esm boundary. Running the deployed entry.js should use ES module semantics and complete successfully.
Environment (please complete the following information):
- OS: Windows
- Python Version: 3.12.13
- APM Version: 0.30.0
Logs
APM reports no installation error. Executing the deployed JavaScript file produces a Node module syntax error such as:
SyntaxError: Cannot use import statement outside a module
Additional context
src/apm_cli/integration/hook_bundle.py excludes every file named package.json in iter_deployable_hook_bundle_files, regardless of target. Claude passes exclude_json_files=False, but that option only controls the broader .json suffix filter and does not restore the skipped filename.
_hook_module_type searches the hook source root and its ancestors, not nested metadata directories. With no package-root or hooks/package.json, it therefore defaults to CommonJS and writes one sidecar at the deployed hook root.
This is distinct from #2824, which concerns missing .claude-plugin/plugin.json and other files outside the selected hook script bundle. This defect concerns module metadata inside an otherwise selected bundle.
Describe the bug
When a Claude hook package places a JavaScript hook and its module metadata in a nested directory, APM deploys the JavaScript file but drops the nested
package.json. APM then generates a CommonJS sidecar at the higher hook root, so Node executes the nested.jsfile with the wrong module semantics.To Reproduce
Steps to reproduce the behavior:
Create a Claude hook package with this layout:
Set
hooks/hooks.jsonto:{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "node ./esm/entry.js" } ] } ] } }Set
hooks/esm/entry.jsto an ES module, for example:Set
hooks/esm/package.jsonto:{ "type": "module" }Install the package into a project with Claude hooks enabled by running
apm install.Inspect the deployed hook under
.claude/hooks/<package>/hooks/esm/.Observe that
entry.jsis present, butesm/package.jsonis missing. The generated.claude/hooks/<package>/hooks/package.jsoncontains{"type": "commonjs"}.Run the deployed hook with Node and observe an import syntax error because the file is treated as CommonJS.
Expected behavior
APM should preserve the nested
hooks/esm/package.json, or generate equivalent module metadata at the deployedesmboundary. Running the deployedentry.jsshould use ES module semantics and complete successfully.Environment (please complete the following information):
Logs
APM reports no installation error. Executing the deployed JavaScript file produces a Node module syntax error such as:
Additional context
src/apm_cli/integration/hook_bundle.pyexcludes every file namedpackage.jsoniniter_deployable_hook_bundle_files, regardless of target. Claude passesexclude_json_files=False, but that option only controls the broader.jsonsuffix filter and does not restore the skipped filename._hook_module_typesearches the hook source root and its ancestors, not nested metadata directories. With no package-root orhooks/package.json, it therefore defaults to CommonJS and writes one sidecar at the deployed hook root.This is distinct from #2824, which concerns missing
.claude-plugin/plugin.jsonand other files outside the selected hook script bundle. This defect concerns module metadata inside an otherwise selected bundle.