# How the Conversion Pipeline Handles Hooks and Events in the Compound Engineering Plugin

> Discover how the Compound Engineering Plugin conversion pipeline processes Claude hooks and events through three stages: loading, mapping, and rendering. Optimize your workflow today.

- Repository: [Every/compound-engineering-plugin](https://github.com/everyinc/compound-engineering-plugin)
- Tags: how-to-guide
- Published: 2026-02-16

---

**The conversion pipeline processes Claude hooks through three stages: loading and merging hook definitions from [`src/parsers/claude.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/parsers/claude.ts), mapping Claude hook events to OpenCode events via the `HOOK_EVENT_MAP` in [`src/converters/claude-to-opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/converters/claude-to-opencode.ts), and rendering TypeScript handler blocks that become executable OpenCode plugins.**

The **Compound Engineering Plugin** converts Claude Code plugins into OpenCode bundles, transforming proprietary hook definitions into standardized event handlers. When a plugin contains Claude-style hooks, the conversion pipeline handles the translation through a systematic three-stage process that preserves the original logic while adapting it to the OpenCode event system.

## Stage 1: Loading and Merging Claude Hook Definitions

The pipeline begins by ingesting hook definitions from the source Claude plugin. The `loadHooks` function in [`src/parsers/claude.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/parsers/claude.ts) (lines 21-45) reads the default [`hooks/hooks.json`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/hooks/hooks.json) file and any additional hook files referenced in the plugin manifest.

```typescript
const hooks = await loadHooks(root, manifest.hooks)   // src/parsers/claude.ts

```

If a `hooks` field is present in [`plugin.json`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/plugin.json) or a [`hooks/hooks.json`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/hooks/hooks.json) file exists, both are read and merged via `mergeHooks` into a single `ClaudeHooks` object. This unified structure serves as the input for the subsequent conversion stages.

## Stage 2: Mapping Claude Hook Events to OpenCode Events

Once loaded, the pipeline translates Claude hook event names into their OpenCode equivalents. The `convertClaudeToOpenCode` function calls `convertHooks`, which references the `HOOK_EVENT_MAP` constant defined in [`src/converters/claude-to-opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/converters/claude-to-opencode.ts) (lines 48-62).

```typescript
const HOOK_EVENT_MAP: Record<string, HookEventMapping> = {
  PreToolUse:   { events: ["tool.execute.before"], type: "tool" },
  PostToolUse:  { events: ["tool.execute.after"],  type: "tool" },
  // … other mappings …
};

```

Each Claude hook name (e.g., `PreToolUse`) maps to one or more OpenCode event identifiers (`tool.execute.before`). The mapping also specifies the handler type—such as `tool`, `session`, `permission`, or `message`—which determines how the subsequent code generation handles conditional logic and action rendering. Unmapped events are collected and added as comments in the generated file for debugging purposes.

## Stage 3: Rendering Hook Handlers

The final stage generates executable TypeScript code. For every matching event, `renderHookHandlers` creates a handler block. The function iterates over Claude matchers, builds conditional statements, and embeds hook actions (`command`, `prompt`, or `agent`).

```typescript
for (const [eventName, matchers] of Object.entries(hookMap)) {
  const mapping = HOOK_EVENT_MAP[eventName];
  if (!mapping) { unmappedEvents.push(eventName); continue; }

  for (const event of mapping.events) {
    handlerBlocks.push(
      renderHookHandlers(event, matchers, {
        useToolMatcher: mapping.type === "tool" || mapping.type === "permission",
        requireError: !!mapping.requireError,
        note: mapping.note,
      })
    );
  }
}

```

When the hook is a *tool* hook, the matcher's `matcher` string is split into individual tool names. If a specific tool list is present, the pipeline generates a condition such as `input.tool === "read" || input.tool === "write"`.

Action rendering depends on the `hook.type`:
- **command** → `await $\`<command>\`` (optionally guarded by the condition)
- **prompt** → a comment describing the prompt
- **agent** → a comment describing the agent hook

The result is a TypeScript object where each OpenCode event is an async handler returning the composed statements. This file ([`converted-hooks.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/converted-hooks.ts)) becomes part of the OpenCode bundle returned by `convertClaudeToOpenCode` and is written to the target output directory via [`src/targets/opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/targets/opencode.ts).

## Code Examples

### Example Claude Hook Definition

Here is a typical Claude hook configuration in [`hooks/hooks.json`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/hooks/hooks.json):

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "read | write",
        "hooks": [
          { "type": "command", "command": "log-start", "timeout": 5 }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "prompt", "prompt": "User said: {{input.message}}" }
        ]
      }
    ]
  }
}

```

### Generated OpenCode Hook File

The pipeline generates the following [`converted-hooks.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/converted-hooks.ts):

```typescript
// Unmapped Claude hook events: … (none in this example)
import type { Plugin } from "@opencode-ai/plugin"

export const ConvertedHooks: Plugin = async ({ $ }) => {
  return {
    "tool.execute.before": async (input) => {
      // PreToolUse → tool.execute.before
      if (input.tool === "read" || input.tool === "write") { await $`log-start` }
      // timeout: 5s (not enforced)
    },
    "message.created": async (input) => {
      // UserPromptSubmit → message.created
      // Prompt hook for *: User said: {{input.message}}
    }
  }
}

export default ConvertedHooks

```

Notice how the `matcher` becomes a conditional `if` statement, and each Claude hook type is turned into a comment or an executable command.

## Summary

- The conversion pipeline processes Claude hooks through three distinct stages: loading and merging definitions, mapping events via `HOOK_EVENT_MAP`, and rendering TypeScript handlers.
- The `loadHooks` function in [`src/parsers/claude.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/parsers/claude.ts) ingests and merges hook files into a unified `ClaudeHooks` object.
- The `HOOK_EVENT_MAP` in [`src/converters/claude-to-opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/converters/claude-to-opencode.ts) translates Claude event names like `PreToolUse` into OpenCode events like `tool.execute.before`.
- The `renderHookHandlers` function generates conditional logic and action blocks, converting `command` hooks into executable shell calls and `prompt`/`agent` hooks into descriptive comments.
- The final output is a [`converted-hooks.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/converted-hooks.ts) file exported as an OpenCode plugin via [`src/targets/opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/targets/opencode.ts).

## Frequently Asked Questions

### How does the conversion pipeline handle unsupported Claude hook events?

Unmapped events are collected during the conversion process and added as comments at the top of the generated [`converted-hooks.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/converted-hooks.ts) file. This allows developers to identify which hooks require manual implementation while ensuring the conversion does not fail on unrecognized event types.

### What is the difference between tool hooks and message hooks in the conversion process?

Tool hooks (such as `PreToolUse` and `PostToolUse`) trigger the `useToolMatcher` flag in `renderHookHandlers`, which generates conditional statements checking `input.tool` against specific tool names. Message hooks (such as `UserPromptSubmit`) map to `message.created` events and typically render as comments describing the prompt content rather than executable conditions.

### Can the conversion pipeline handle multiple hook files?

Yes. The `loadHooks` function in [`src/parsers/claude.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/parsers/claude.ts) reads both the default [`hooks/hooks.json`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/hooks/hooks.json) file and any additional hook files referenced in the plugin manifest's `hooks` field. The `mergeHooks` utility combines these into a single `ClaudeHooks` object, allowing distributed hook definitions to be consolidated before conversion.

### How are command timeouts handled in the generated OpenCode plugin?

The conversion pipeline preserves timeout values as comments in the generated code (e.g., `// timeout: 5s (not enforced)`), but does not implement automatic timeout enforcement in the rendered handler. Developers must manually implement timeout logic in the OpenCode environment if strict timeout control is required for command hooks.