Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1610f24
Initial plan
Copilot Feb 23, 2026
296c17b
Plan: Add HookIntegrator for hooks primitive support
Copilot Feb 23, 2026
4f76bf1
feat: add HookIntegrator for hooks primitive support
Copilot Feb 23, 2026
205542f
docs: add hooks as a supported primitive type in primitives and integ…
Copilot Feb 23, 2026
e98d6ca
fix: simplify relative path regex in hook_integrator and add safety c…
Copilot Feb 23, 2026
213eb7e
Merge branch 'main' into copilot/add-support-for-hooks
danielmeppiel Feb 24, 2026
83a1f6d
Merge branch 'main' into copilot/add-support-for-hooks
danielmeppiel Feb 25, 2026
97663dc
Merge branch 'main' into copilot/add-support-for-hooks
danielmeppiel Feb 25, 2026
a0876b9
Merge branch 'main' into copilot/add-support-for-hooks
danielmeppiel Feb 26, 2026
7987227
fix: validation gate, path resolution, uninstall import, and deps hoo…
danielmeppiel Feb 27, 2026
a04ec9d
Merge main: resolve conflicts, preserve hooks support
danielmeppiel Feb 27, 2026
d49d97d
docs: add hooks coverage across all documentation
danielmeppiel Feb 27, 2026
d0dbd35
docs: add Hooks to README primitives table and .apm/ structure
danielmeppiel Feb 27, 2026
734c32b
revert: remove CHANGELOG.md changes, will be handled separately
danielmeppiel Feb 27, 2026
9f81b41
security: reject path traversal in hook script resolution
danielmeppiel Feb 27, 2026
39e4c4d
fix: handle bash/powershell keys in hook path rewriter
danielmeppiel Mar 2, 2026
3b64d2e
Merge branch 'main' into copilot/add-support-for-hooks
danielmeppiel Mar 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ apm install danielmeppiel/design-guidelines

# Agents are automatically integrated to:
# .github/agents/*-apm.agent.md (verbatim copy)

# Hooks are automatically integrated to:
# .github/hooks/*-apm.json (hook definitions with rewritten script paths)
```

**How Auto-Integration Works**:
Expand All @@ -163,13 +166,14 @@ apm install danielmeppiel/design-guidelines

**Integration Flow**:
1. Run `apm install` to fetch APM packages
2. APM automatically creates `.github/prompts/` and `.github/agents/` directories if needed
3. Discovers `.prompt.md` and `.agent.md` files in each package
2. APM automatically creates `.github/prompts/`, `.github/agents/`, and `.github/hooks/` directories if needed
3. Discovers `.prompt.md`, `.agent.md`, and hook `.json` files in each package
4. Copies prompts to `.github/prompts/` with `-apm` suffix (e.g., `accessibility-audit-apm.prompt.md`)
5. Copies agents to `.github/agents/` with `-apm` suffix (e.g., `security-apm.agent.md`)
6. Updates `.gitignore` to exclude integrated prompts and agents
7. VSCode automatically loads all prompts and agents for your coding agents
8. Run `apm uninstall` to automatically remove integrated prompts and agents
6. Copies hooks to `.github/hooks/` with `-apm` suffix (e.g., `hookify-hooks-apm.json`) and copies referenced scripts
7. Updates `.gitignore` to exclude integrated prompts, agents, and hooks
8. VSCode automatically loads all prompts, agents, and hooks for your coding agents
9. Run `apm uninstall` to automatically remove integrated primitives

**Intent-First Discovery**:
The `-apm` suffix pattern enables natural autocomplete in VSCode:
Expand Down Expand Up @@ -225,6 +229,7 @@ When you run `apm install`, APM integrates package primitives into Claude's nati
|----------|---------||
| `.claude/commands/*.md` | Slash commands from installed packages (from `.prompt.md` files) |
| `.github/skills/{folder}/` | Skills from packages with `SKILL.md` or `.apm/` primitives |
| `.claude/settings.json` (hooks key) | Hooks from installed packages (merged into settings) |

### Automatic Command Integration

Expand Down Expand Up @@ -268,6 +273,31 @@ apm install ComposioHQ/awesome-claude-skills/mcp-builder
4. Updates `.gitignore` to exclude generated skills
5. `apm uninstall` removes the skill folder

### Automatic Hook Integration

APM automatically integrates hooks from installed packages. Hooks define lifecycle event handlers (e.g., `PreToolUse`, `PostToolUse`, `Stop`) supported by both VSCode Copilot and Claude Code.

```bash
# Install a package with hooks
apm install anthropics/claude-plugins-official/plugins/hookify

# VSCode result (.github/hooks/):
# .github/hooks/hookify-hooks-apm.json β†’ Hook definitions
# .github/hooks/scripts/hookify/hooks/*.py β†’ Referenced scripts

# Claude result (.claude/settings.json):
# Hooks merged into .claude/settings.json hooks key
# Scripts copied to .claude/hooks/hookify/
```

**How hook integration works:**
1. `apm install` discovers hook JSON files in `.apm/hooks/` or `hooks/` directories
2. For VSCode: copies hook JSON to `.github/hooks/` with `-apm` suffix and rewrites script paths
3. For Claude: merges hook definitions into `.claude/settings.json` under the `hooks` key
4. Copies referenced scripts to the target location
5. Rewrites `${CLAUDE_PLUGIN_ROOT}` and relative script paths for the target platform
6. `apm uninstall` removes hook files and cleans up merged settings

### Target-Specific Compilation

Generate only Claude formats when needed:
Expand Down
9 changes: 7 additions & 2 deletions docs/primitives.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ The APM CLI supports four types of primitives:
- **Instructions** (`.instructions.md`) - Provide coding standards and guidelines for specific file types
- **Skills** (`SKILL.md`) - Package meta-guides that help AI agents understand what a package does
- **Context** (`.context.md`, `.memory.md`) - Supply background information and project context
- **Hooks** (`.json` in `.apm/hooks/` or `hooks/`) - Define lifecycle event handlers with script references

> **Note**: Both `.agent.md` (new format) and `.chatmode.md` (legacy format) are fully supported. VSCode provides Quick Fix actions to help migrate from `.chatmode.md` to `.agent.md`.

Expand All @@ -95,8 +96,12 @@ APM discovers primitives in these locations:
β”‚ └── *.instructions.md
β”œβ”€β”€ context/ # Project context files
β”‚ └── *.context.md
└── memory/ # Team info, contacts, etc.
└── *.memory.md
β”œβ”€β”€ memory/ # Team info, contacts, etc.
β”‚ └── *.memory.md
└── hooks/ # Lifecycle event handlers
β”œβ”€β”€ *.json # Hook definitions (JSON)
└── scripts/ # Referenced scripts
└── *.sh, *.py

# VSCode-compatible structure
.github/
Expand Down
74 changes: 73 additions & 1 deletion src/apm_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,13 +1162,16 @@ def uninstall(ctx, packages, dry_run):
commands_failed = 0
skills_cleaned = 0
skills_failed = 0
hooks_cleaned = 0
hooks_failed = 0

try:
from apm_cli.models.apm_package import APMPackage, PackageInfo, PackageType, validate_package
from apm_cli.integration.prompt_integrator import PromptIntegrator
from apm_cli.integration.agent_integrator import AgentIntegrator
from apm_cli.integration.skill_integrator import SkillIntegrator
from apm_cli.integration.command_integrator import CommandIntegrator
from apm_cli.integration.hook_integrator import HookIntegrator

apm_package = APMPackage.from_apm_yml(Path("apm.yml"))
project_root = Path(".")
Expand All @@ -1194,11 +1197,17 @@ def uninstall(ctx, packages, dry_run):
result = integrator.sync_integration(apm_package, project_root)
commands_cleaned = result.get("files_removed", 0)

# Clean hooks (.github/hooks/ and .claude/settings.json)
hook_integrator_cleanup = HookIntegrator()
result = hook_integrator_cleanup.sync_integration(apm_package, project_root)
hooks_cleaned = result.get("files_removed", 0)

# Phase 2: Re-integrate from remaining installed packages in apm_modules/
prompt_integrator = PromptIntegrator()
agent_integrator = AgentIntegrator()
skill_integrator = SkillIntegrator()
command_integrator = CommandIntegrator()
hook_integrator_reint = HookIntegrator()

for dep in apm_package.get_apm_dependencies():
dep_ref = dep if hasattr(dep, 'repo_url') else None
Expand Down Expand Up @@ -1231,6 +1240,8 @@ def uninstall(ctx, packages, dry_run):
skill_integrator.integrate_package_skill(pkg_info, project_root)
if command_integrator.should_integrate(project_root):
command_integrator.integrate_package_commands(pkg_info, project_root)
hook_integrator_reint.integrate_package_hooks(pkg_info, project_root)
hook_integrator_reint.integrate_package_hooks_claude(pkg_info, project_root)
except Exception:
pass # Best effort re-integration

Expand All @@ -1246,14 +1257,17 @@ def uninstall(ctx, packages, dry_run):
_rich_info(f"βœ“ Cleaned up {skills_cleaned} skill(s)")
if commands_cleaned > 0:
_rich_info(f"βœ“ Cleaned up {commands_cleaned} command(s)")
if hooks_cleaned > 0:
_rich_info(f"βœ“ Cleaned up {hooks_cleaned} hook(s)")
if (
prompts_failed > 0
or agents_failed > 0
or skills_failed > 0
or commands_failed > 0
or hooks_failed > 0
):
_rich_warning(
f"⚠ Failed to clean up {prompts_failed + agents_failed + skills_failed + commands_failed} file(s)"
f"⚠ Failed to clean up {prompts_failed + agents_failed + skills_failed + commands_failed + hooks_failed} file(s)"
)

# Final summary
Expand Down Expand Up @@ -1453,13 +1467,16 @@ def matches_filter(dep):
agent_integrator = AgentIntegrator()
from apm_cli.integration.skill_integrator import SkillIntegrator, should_install_skill
from apm_cli.integration.command_integrator import CommandIntegrator
from apm_cli.integration.hook_integrator import HookIntegrator

skill_integrator = SkillIntegrator()
command_integrator = CommandIntegrator()
hook_integrator = HookIntegrator()
total_prompts_integrated = 0
total_agents_integrated = 0
total_skills_generated = 0
total_commands_integrated = 0
total_hooks_integrated = 0
total_links_resolved = 0

# Collect installed packages for lockfile generation
Expand Down Expand Up @@ -1658,6 +1675,26 @@ def matches_filter(dep):
f" └─ {command_result.files_updated} commands updated"
)
total_links_resolved += command_result.links_resolved

# Hook integration (target-aware)
if integrate_vscode:
hook_result = hook_integrator.integrate_package_hooks(
cached_package_info, project_root
)
if hook_result.hooks_integrated > 0:
total_hooks_integrated += hook_result.hooks_integrated
_rich_info(
f" └─ {hook_result.hooks_integrated} hook(s) integrated β†’ .github/hooks/"
)
if integrate_claude:
hook_result_claude = hook_integrator.integrate_package_hooks_claude(
cached_package_info, project_root
)
if hook_result_claude.hooks_integrated > 0:
total_hooks_integrated += hook_result_claude.hooks_integrated
_rich_info(
f" └─ {hook_result_claude.hooks_integrated} hook(s) integrated β†’ .claude/settings.json"
)
except Exception as e:
# Don't fail installation if integration fails
_rich_warning(
Expand Down Expand Up @@ -1812,6 +1849,26 @@ def matches_filter(dep):
f" └─ {command_result.files_updated} commands updated"
)
total_links_resolved += command_result.links_resolved

# Hook integration (target-aware)
if integrate_vscode:
hook_result = hook_integrator.integrate_package_hooks(
package_info, project_root
)
if hook_result.hooks_integrated > 0:
total_hooks_integrated += hook_result.hooks_integrated
_rich_info(
f" └─ {hook_result.hooks_integrated} hook(s) integrated β†’ .github/hooks/"
)
if integrate_claude:
hook_result_claude = hook_integrator.integrate_package_hooks_claude(
package_info, project_root
)
if hook_result_claude.hooks_integrated > 0:
total_hooks_integrated += hook_result_claude.hooks_integrated
_rich_info(
f" └─ {hook_result_claude.hooks_integrated} hook(s) integrated β†’ .claude/settings.json"
)
except Exception as e:
# Don't fail installation if integration fails
_rich_warning(f" ⚠ Failed to integrate primitives: {e}")
Expand Down Expand Up @@ -1866,6 +1923,17 @@ def matches_filter(dep):
except Exception as e:
_rich_warning(f"Could not update .gitignore for agents: {e}")

# Update .gitignore for integrated hooks if any were integrated
if integrate_vscode and total_hooks_integrated > 0:
try:
updated = hook_integrator.update_gitignore(project_root)
if updated:
_rich_info(
"Updated .gitignore for integrated hooks (*-apm.json)"
)
except Exception as e:
_rich_warning(f"Could not update .gitignore for hooks: {e}")

# Show link resolution stats if any were resolved
if total_links_resolved > 0:
_rich_info(f"βœ“ Resolved {total_links_resolved} context file links")
Expand All @@ -1878,6 +1946,10 @@ def matches_filter(dep):
if total_commands_integrated > 0:
_rich_info(f"βœ“ Integrated {total_commands_integrated} command(s)")

# Show hooks stats if any were integrated
if total_hooks_integrated > 0:
_rich_info(f"βœ“ Integrated {total_hooks_integrated} hook(s)")

_rich_success(f"Installed {installed_count} APM dependencies")

return installed_count, total_prompts_integrated, total_agents_integrated
Expand Down
11 changes: 11 additions & 0 deletions src/apm_cli/deps/package_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ def validate_package_structure(self, package_path: Path) -> ValidationResult:
# Validate each primitive file
for md_file in md_files:
self._validate_primitive_file(md_file, result)

# Check for hooks (JSON files, not markdown)
hooks_dir = apm_dir / "hooks"
if hooks_dir.exists() and hooks_dir.is_dir():
json_files = list(hooks_dir.glob("*.json"))
if json_files:
has_primitives = True

if not has_primitives:
result.add_warning("No primitive files found in .apm/ directory")
Expand Down Expand Up @@ -209,6 +216,10 @@ def get_package_info_summary(self, package_path: Path) -> Optional[str]:
primitive_dir = apm_dir / primitive_type
if primitive_dir.exists():
primitive_count += len(list(primitive_dir.glob("*.md")))
# Count hook files
hooks_dir = apm_dir / "hooks"
if hooks_dir.exists():
primitive_count += len(list(hooks_dir.glob("*.json")))

if primitive_count > 0:
summary += f" ({primitive_count} primitives)"
Expand Down
2 changes: 2 additions & 0 deletions src/apm_cli/integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .prompt_integrator import PromptIntegrator
from .agent_integrator import AgentIntegrator
from .hook_integrator import HookIntegrator
from .skill_integrator import (
SkillIntegrator,
validate_skill_name,
Expand All @@ -17,6 +18,7 @@
__all__ = [
'PromptIntegrator',
'AgentIntegrator',
'HookIntegrator',
'SkillIntegrator',
'SkillTransformer',
'validate_skill_name',
Expand Down
Loading