Skip to content

Install silently drops skills/agents/commands when package also ships hooks/*.json #780

@danielmeppiel

Description

@danielmeppiel

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugDeprecated: use type/bug. Kept for issue history; will be removed in milestone 0.10.0.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions