Skip to content

Hooks and commands

Hooks and slash commands are the two APM primitives that do not pretend to be portable. Unlike skills or instructions, they ship to a strict subset of harnesses, never get folded into AGENTS.md, and rely on each target’s own format. Author them when the value to a specific harness justifies the per-target maintenance.

This page covers both. For the cross-harness reach map, see Primitives and targets. For dev-only versus prod separation in the manifest, see Dev-only primitives.

A skill is a markdown file APM can route to seven harnesses. A hook is a runtime callback fired by one harness inside its own tool loop. A slash command is a command-palette entry surfaced by an IDE. Neither generalizes: nothing reaches AGENTS.md, nothing routes to harnesses that lack the concept. Unsupported targets are silently skipped, not errors. Treat both as opt-in surface, not as your primary distribution path.

Source layout. APM discovers hook JSON files in either of two directories at the package root:

your-package/
+-- .apm/
| +-- hooks/
| +-- pretool-validate.json
+-- hooks/ # also discovered (Claude-native layout)
+-- post-edit-format.json

Each file is a JSON document keyed by lifecycle event. APM accepts the Claude (PreToolUse, PostToolUse) and Copilot (preToolUse, postToolUse) shapes; events are renamed per target during merge.

{
"hooks": {
"PreToolUse": [
{
"hooks": [
{"type": "command", "command": "${PLUGIN_ROOT}/scripts/validate.sh", "timeout": 10}
]
}
]
}
}

The ${PLUGIN_ROOT}, ${CLAUDE_PLUGIN_ROOT}, and ${CURSOR_PLUGIN_ROOT} tokens resolve to the installed package root and are rewritten per target. Plain ./script.sh resolves relative to the hook file.

Supported targets and where the integrator writes:

TargetOutputMode
copilot.github/hooks/<pkg>-<name>.jsonone file per hook
claude.claude/settings.jsonmerged into settings
cursor.cursor/hooks.jsonmerged
gemini.gemini/settings.jsonmerged
codex.codex/hooks.jsonmerged
windsurf.windsurf/hooks.jsonmerged
opencode— not supported —silently skipped

Copilot hook files are namespaced with the source package name to avoid collisions across installed deps; bundled scripts land alongside under .github/hooks/scripts/<pkg>/.

Verified against src/apm_cli/integration/targets.py and src/apm_cli/integration/hook_integrator.py.

Slash commands share their source with prompts. There is no .apm/commands/ directory:

your-package/
+-- .apm/
+-- prompts/
+-- review-pr.prompt.md # also routes as a slash command

Frontmatter the command integrator preserves: description, allowed-tools, model, argument-hint, input. Other keys (for example author, mcp, parameters) are dropped during the transform and surfaced as install-time diagnostics.

---
description: Review the current PR for security regressions.
allowed-tools: [Read, Grep]
argument-hint: "[pr-number]"
input:
- name: pr_number
description: The PR number to review.
---
Review pull request #$pr_number. Focus on auth and input handling.

Supported targets and output paths:

TargetOutputFormat
claude.claude/commands/<name>.mdnative markdown
cursor.cursor/commands/<name>.mdclaude-format subset
opencode.opencode/commands/<name>.mdopencode markdown
gemini.gemini/commands/<name>.tomlTOML
windsurf.windsurf/workflows/<name>.mdcalled “workflows”
copilot— not a command —ships as a prompt
codex— not supported —silently skipped

Verified against src/apm_cli/integration/targets.py and src/apm_cli/integration/command_integrator.py.

Reach for a skill, instruction, or prompt first. Use hooks or commands only when the behavior is a runtime callback (hook) or a command-palette entry (command), you accept that consumers on Copilot, Codex, or OpenCode will not get them, and you will own per-target formats. “Run a script before every tool call” fits a hook. “Give the agent a procedure” fits a skill — and reaches every harness.

  • Hook event names. Author in Claude or Copilot conventions only. The integrator renames; arbitrary event names will not be mapped.
  • Cursor command frontmatter loss. Cursor reuses the Claude command transformer today, so any prompt-only metadata is dropped with a diagnostic. Keep Cursor commands to the preserved key set.
  • Script paths. Use ${PLUGIN_ROOT} (or the harness-specific alias) for scripts that ship inside the package. Plain absolute paths break on consumers’ machines.
  • Same .prompt.md is two primitives. A single .apm/prompts/foo.prompt.md becomes Copilot’s prompt and Claude’s /foo command in the same install. Name files with both surfaces in mind.
  • OpenCode and hooks. OpenCode has no hooks concept. Do not author a Claude+OpenCode package and assume hooks reach both — they do not. The install log notes the skip.

Once your hooks and commands are in place, run apm compile to preview what each target will receive, then apm pack to bundle. See Compile and Pack a bundle.