Summary
apm install obra/superpowers deploys only the 2 hook files and silently skips the package's 14 skills, 1 agent, and 3 commands. The package is misclassified as HOOK_PACKAGE even though it ships a complete Claude Code plugin layout (.claude-plugin/plugin.json + agents/ + skills/ + commands/ + hooks/).
Reproduced on macOS, with the latest main (also confirmed against the released CLI).
Reproducer
apm install obra/superpowers
Result reported by apm install:
Hooks 2 ~/.copilot/hooks/
Expected: 14 skills + 1 agent + 3 commands also deployed (the package is a fully-featured Claude Code plugin -- see https://github.com/obra/superpowers).
Root cause
The detection cascade in src/apm_cli/models/validation.py:138-174 (detect_package_type) is order-sensitive and the order is wrong when a package ships both hook files and a richer plugin layout:
# Current order:
1. HYBRID (apm.yml + SKILL.md)
2. APM_PACKAGE (apm.yml)
3. CLAUDE_SKILL (SKILL.md)
4. HOOK_PACKAGE (hooks/*.json) <-- fires here for superpowers
5. MARKETPLACE_PLUGIN (plugin.json OR agents/ OR skills/ OR commands/)
obra/superpowers matches both rule 4 (hooks/hooks.json) and rule 5 (.claude-plugin/plugin.json, agents/, skills/, commands/). Because rule 4 fires first, the package is classified HOOK_PACKAGE, which does not trigger normalize_plugin_directory() in src/apm_cli/install/sources.py:178-180 -- the call that would synthesize apm.yml from the plugin layout and surface skills/agents/commands to the rest of the install pipeline. Result: only hooks ship.
Verified by direct invocation against a fresh clone:
from pathlib import Path
from apm_cli.models.validation import detect_package_type
print(detect_package_type(Path("/tmp/superpowers-repro")))
# (<PackageType.HOOK_PACKAGE: 'hook_package'>, None)
The repo layout that triggers the bug:
.claude-plugin/
plugin.json <-- triggers rule 5
marketplace.json
agents/code-reviewer.md <-- triggers rule 5
skills/... <-- 14 skills, triggers rule 5
commands/... <-- 3 commands, triggers rule 5
hooks/
hooks.json <-- triggers rule 4 (wins)
hooks-cursor.json
session-start
Fix proposal
Invert priority between HOOK_PACKAGE and MARKETPLACE_PLUGIN. The marketplace-plugin path is a superset -- normalize_plugin_directory -> synthesize_apm_yml_from_plugin -> _map_plugin_artifacts already handles hooks alongside agents/skills/commands. HOOK_PACKAGE should only fire when there is no plugin evidence at all (true hooks-only packages).
# Proposed order in detect_package_type:
1. HYBRID
2. APM_PACKAGE
3. CLAUDE_SKILL
4. MARKETPLACE_PLUGIN (plugin.json OR agents/ OR skills/ OR commands/) <-- moved up
5. HOOK_PACKAGE (hooks/*.json) <-- fallback
Diff:
# Before:
if _has_hook_json(package_path):
return PackageType.HOOK_PACKAGE, None
plugin_json_path = find_plugin_json(package_path)
has_plugin_evidence = (
plugin_json_path is not None
or (package_path / "agents").is_dir()
or (package_path / "skills").is_dir()
or (package_path / "commands").is_dir()
)
if has_plugin_evidence:
return PackageType.MARKETPLACE_PLUGIN, plugin_json_path
# After:
plugin_json_path = find_plugin_json(package_path)
has_plugin_evidence = (
plugin_json_path is not None
or (package_path / "agents").is_dir()
or (package_path / "skills").is_dir()
or (package_path / "commands").is_dir()
)
if has_plugin_evidence:
return PackageType.MARKETPLACE_PLUGIN, plugin_json_path
if _has_hook_json(package_path):
return PackageType.HOOK_PACKAGE, None
Test coverage to add
A regression test in tests/unit/models/test_validation.py (or wherever test_detect_package_type lives) covering a fixture that mirrors the superpowers layout: hooks/hooks.json + .claude-plugin/plugin.json + agents/ + skills/ + commands/. Asserts MARKETPLACE_PLUGIN, not HOOK_PACKAGE. Bonus: a hooks-only fixture (just hooks/hooks.json) to assert HOOK_PACKAGE still wins when there's no plugin evidence.
Blast radius
Any package that ships hooks alongside a Claude Code plugin layout is currently truncated to hooks-only on install. As .claude-plugin/ packages proliferate (the Anthropic spec encourages bundling hooks with skills/agents), this is going to silently swallow content for any user who installs them via APM. Suggest treating as a P1 -- the failure mode is silent data loss from the user's perspective ("APM said it installed it, but the skills aren't there").
Related
src/apm_cli/models/validation.py:138-174 -- detect_package_type
src/apm_cli/install/sources.py:175-180 -- normalize_plugin_directory invocation gated by MARKETPLACE_PLUGIN
src/apm_cli/deps/plugin_parser.py:83-155 -- normalize_plugin_directory and synthesize_apm_yml_from_plugin
Reported originally in community discussion. The fix is small (one block reorder + a test), and the contributor experience cost of leaving it is high -- this is exactly the kind of "I tried APM and it ate half my package" first-impression bug we cannot afford.
Summary
apm install obra/superpowersdeploys only the 2 hook files and silently skips the package's 14 skills, 1 agent, and 3 commands. The package is misclassified asHOOK_PACKAGEeven though it ships a complete Claude Code plugin layout (.claude-plugin/plugin.json+agents/+skills/+commands/+hooks/).Reproduced on macOS, with the latest
main(also confirmed against the released CLI).Reproducer
Result reported by
apm install:Expected: 14 skills + 1 agent + 3 commands also deployed (the package is a fully-featured Claude Code plugin -- see https://github.com/obra/superpowers).
Root cause
The detection cascade in
src/apm_cli/models/validation.py:138-174(detect_package_type) is order-sensitive and the order is wrong when a package ships both hook files and a richer plugin layout:obra/superpowersmatches both rule 4 (hooks/hooks.json) and rule 5 (.claude-plugin/plugin.json,agents/,skills/,commands/). Because rule 4 fires first, the package is classifiedHOOK_PACKAGE, which does not triggernormalize_plugin_directory()insrc/apm_cli/install/sources.py:178-180-- the call that would synthesizeapm.ymlfrom the plugin layout and surface skills/agents/commands to the rest of the install pipeline. Result: only hooks ship.Verified by direct invocation against a fresh clone:
The repo layout that triggers the bug:
Fix proposal
Invert priority between
HOOK_PACKAGEandMARKETPLACE_PLUGIN. The marketplace-plugin path is a superset --normalize_plugin_directory->synthesize_apm_yml_from_plugin->_map_plugin_artifactsalready handles hooks alongside agents/skills/commands.HOOK_PACKAGEshould only fire when there is no plugin evidence at all (true hooks-only packages).Diff:
Test coverage to add
A regression test in
tests/unit/models/test_validation.py(or wherevertest_detect_package_typelives) covering a fixture that mirrors the superpowers layout:hooks/hooks.json+.claude-plugin/plugin.json+agents/+skills/+commands/. AssertsMARKETPLACE_PLUGIN, notHOOK_PACKAGE. Bonus: a hooks-only fixture (justhooks/hooks.json) to assertHOOK_PACKAGEstill wins when there's no plugin evidence.Blast radius
Any package that ships hooks alongside a Claude Code plugin layout is currently truncated to hooks-only on install. As
.claude-plugin/packages proliferate (the Anthropic spec encourages bundling hooks with skills/agents), this is going to silently swallow content for any user who installs them via APM. Suggest treating as a P1 -- the failure mode is silent data loss from the user's perspective ("APM said it installed it, but the skills aren't there").Related
src/apm_cli/models/validation.py:138-174--detect_package_typesrc/apm_cli/install/sources.py:175-180--normalize_plugin_directoryinvocation gated byMARKETPLACE_PLUGINsrc/apm_cli/deps/plugin_parser.py:83-155--normalize_plugin_directoryandsynthesize_apm_yml_from_pluginReported originally in community discussion. The fix is small (one block reorder + a test), and the contributor experience cost of leaving it is high -- this is exactly the kind of "I tried APM and it ate half my package" first-impression bug we cannot afford.