Skip to content

Claude Code compatibility

Package: packages/agent-claude-code-compat

Load a single Claude Code plugin from disk and return the equivalent AlphaCRO {@link Plugin}. The returned plugin contributes:

  • One {@link SlashCommandDefinition} per commands/*.md.
  • One {@link SlashCommandDefinition} per skills/<name>/SKILL.md (skills are invoked the same way as commands but their body is markdown documentation; the host may also surface them in the system prompt via the returned skills metadata).
  • One {@link SubagentDefinition} per agents/*.md.
  • Hook matchers for every supported lifecycle event.

MCP server configs and LSP servers are surfaced on the returned discovered blob but not auto-launched — that’s a downstream MCP/LSP plugin’s job.

ContributionDeclared
Tools
Hooks
Sub-agents
Slash commands
HTTP routes
RPC methods
Workflows
Permissions
Tool cards
State slice
Reducer
Session start handler
Session end handler
Startup phase
Dependencies
  • PreToolUse
  • PostToolUse
  • SessionStart
  • UserPromptSubmit
  • Stop
  • SessionEnd
  • PreCompact
  • Notification
  • SubagentStart
  • SubagentStop

Load real Claude Code plugins straight into the AlphaCRO runtime as Plugin<TState> contributions.

The package is part of the 4-layer architecture:

  • Layer 1 (@alphacro/agent-core) defines the Plugin shape.
  • Layer 2 is where this package lives: a first-party plugin adapter that converts the Claude Code file format (.claude-plugin/plugin.json, commands/*.md, agents/*.md, hooks/hooks.json, skills/<name>/SKILL.md, .mcp.json) into the shape the runtime understands.
  • Layer 3 runtime adapters (CF Worker, Node CLI) get to compose these plugins alongside their own first-party ones.
  • Layer 4 apps (editor-web, agent-cli) just call composePlugins({ plugins: [...] }).
import { composePlugins } from "@alphacro/agent-core";
import { loadClaudeCodePlugin } from "@alphacro/agent-claude-code-compat";
const { plugin } = await loadClaudeCodePlugin(
"/abs/path/to/anthropics/claude-code/plugins/commit-commands",
);
const composed = composePlugins({ plugins: [plugin] });
// composed.slashCommands now includes:
// "commit-commands:clean_gone"
// "commit-commands:commit"
// "commit-commands:commit-push-pr"

To load every plugin in a marketplace-style directory in one call:

import { loadClaudeCodePluginsFrom } from "@alphacro/agent-claude-code-compat";
const all = await loadClaudeCodePluginsFrom(
"/abs/path/to/anthropics/claude-code/plugins",
);
// → Plugin[] for commit-commands, code-review, feature-dev,
// security-guidance, explanatory-output-style, …
Claude Code surfaceAlphaCRO surfaceNotes
.claude-plugin/plugin.jsonPlugin.name / version / descriptionPlugin name is namespaced @alphacro/agent-claude-code-compat:<plugin-name>; slash commands and skills are namespaced <plugin>:<command>.
commands/*.mdslashCommands()Frontmatter (description, argument-hint, allowed-tools, model) is parsed; the handler returns the body with $ARGUMENTS, $1..$N, and !`shell` substituted.
agents/*.mdsubagents()name, description, tools, disallowedTools, model, effort, maxTurns, isolation, background are honoured. hooks / mcpServers / permissionMode in plugin agents are recorded as unsupported[].
hooks/hooks.json (or inline)hooks()command-type hooks are wired into the AlphaCRO hook framework. Decision JSON on stdout (hookSpecificOutput) and exit code 2 (deny) are interpreted. Unsupported hook types (http, mcp_tool, prompt, agent) are skipped with a soft issue.
skills/<name>/SKILL.mdslashCommands()Each skill becomes a namespaced slash command whose handler returns the markdown body. The parsed metadata is still on DiscoveredPlugin.skills so a host that wants to inject skill descriptions into the system prompt can do so.
.mcp.json (or inline)DiscoveredPlugin.mcpServersParsed and surfaced so a downstream MCP plugin can launch them. This package does not spawn processes itself.

Themes, output styles, monitors, LSP servers and inline channels are parsed by the manifest schema but ignored at runtime — those surfaces don’t have a direct AlphaCRO equivalent today.

Claude Code’s hook event names line up almost 1:1 with AlphaCRO’s 17-hook surface. The compat layer maps:

Claude Code eventAlphaCRO HookName
SessionStartSessionStart
SessionEndSessionEnd
UserPromptSubmitUserPromptSubmit
PreToolUsePreToolUse
PostToolUsePostToolUse
Stop / StopFailureStop
PreCompactPreCompact
SubagentStartSubagentStart
SubagentStopSubagentStop
NotificationNotification

Events that don’t have a direct equivalent (e.g. TaskCreated, CwdChanged, WorktreeCreate, FileChanged) are parsed but not wired in — they’re available on DiscoveredPlugin.hooks for hosts that want to add bespoke handling.

Inside hook commands, MCP configs, and skill bodies the loader expands the standard Claude Code variables:

  • ${CLAUDE_PLUGIN_ROOT} — absolute path to the plugin install dir.
  • ${CLAUDE_PLUGIN_DATA} — persistent data dir (when supplied).
  • ${CLAUDE_PROJECT_DIR} — user’s cwd (when supplied).
  • Plus any extra map the host passes to {@link expandPluginPathVariables}.

The same variables are also exported as environment variables to spawned hook processes by the default Node runner.

The default hook command runner shells out via Node’s child_process. Cloudflare Workers / edge runtimes can either:

  1. Skip hooks entirely (pass hookCommandRunner: async () => ({ exitCode: 0, stdout: "", stderr: "" })).
  2. Provide a runner that proxies the hook into a sandbox process (e.g. @cloudflare/sandbox startProcess + capture stdout).

The rest of the package is pure TypeScript with no Cloudflare or Node-only imports outside discover.ts (which uses node:fs/promises) and hook-executor.ts (which uses node:child_process). Hosts that don’t need filesystem discovery can build a DiscoveredPlugin directly from in-memory sources and feed it through the loader’s internals.

We don’t vendor upstream Claude Code plugins into this repo. Instead the test suite has two complementary layers:

Layer A — hand-written feature-targeted fixtures

Section titled “Layer A — hand-written feature-targeted fixtures”

src/__fixtures__/plugins/ ships five small single-purpose fixtures, one per parser/loader feature. Each fixture is the minimum content needed to exercise its surface and stays under the package’s own maintenance:

FixtureFeature it locks down
commands-fixtureSlash commands, $ARGUMENTS / $1..$N substitution, !`shell` blocks, allowed-tools array, argument-hint (string + array).
subagents-fixtureSub-agent frontmatter — tools, disallowedTools, model, isolation: worktree, and the unsupported[] warning surface for hooks / mcpServers / permissionMode.
pretooluse-hook-pythonhooks/hooks.json with a real python3 PreToolUse hook (exit-code 2 deny path).
sessionstart-hook-bashhooks/hooks.json with a real bash SessionStart hook returning hookSpecificOutput.additionalContext.
skill-fixtureskills/<name>/SKILL.md parsing + slash-command namespacing.

Integration tests compose pretooluse-hook-python into the real AgentRunner and drive the runner with a scripted mock model that attempts a forbidden Write — proving the Python hook can block a tool call end-to-end.

src/__tests__/upstream.test.ts proves the loader can parse every real plugin Anthropic ships at a pinned commit of anthropics/claude-code, without us vendoring any upstream code into the repo:

  • scripts/upstream-pin.json holds the single source of truth for the pinned commit SHA (read by both scripts/fetch-upstream-plugins.mjs and the test). Bump it to upgrade the snapshot.
  • pnpm --filter @alphacro/agent-claude-code-compat fetch-upstream (also wired as the package’s pretest hook) downloads the tarball into .upstream-cache/<sha>/plugins/ — outside src/ and excluded via .gitignore.
  • The snapshot test asserts (a) the loader surfaces no parse issues across the whole tree, and (b) each well-known plugin surface still parses correctly (security-guidance PreToolUse hook, explanatory-output-style SessionStart hook, claude-opus-4-5-migration skill, feature-dev sub-agents).
  • If the cache is missing — e.g. CI has no outbound network — the suite skips itself; the Layer A fixtures still lock the parser paths down on their own.

Real upstream agent files routinely ship multi-line unquoted description: values that strict YAML rejects (literal <example> blocks, blank lines, even assistant: lines at column 0). When strict YAML fails the agent loader falls back to the tolerant key-collecting parser in parseFrontmatterLoose to mirror Claude Code’s own line-oriented frontmatter handling.