Extensions subscribe to lifecycle events with `fp.on(event, handler)`. Hooks fall into two categories:

**Pre-hooks** run before the action is committed. Return a `HookValidationError` to block the operation, or `undefined` to allow it.

**Post-hooks** run after the action succeeds. They return `void` and are used for side effects.

## Event List

See the [Lifecycle Events](/docs/extensions/api-reference/#lifecycle-events) section in the API Reference for the complete table of events, context types, and descriptions.

`on()` also accepts arbitrary event strings for forward compatibility with future events.

## Blocking with HookValidationError

Pre-hooks can return an error object to prevent the operation:

```ts
fp.on("issue:status:changing", ({ issue, from, to }) => {
  if (from === "todo" && to === "done") {
    return {
      code: "NO_SKIP_IN_PROGRESS",
      message: "Issues must go through in-progress before done",
    };
  }
  return undefined;
});
```

The `HookValidationError` shape:

```ts
interface HookValidationError {
  code: string;
  message: string;
  details?: Record<string, unknown>;
}
```

## Execution Order

When multiple extensions register the same pre-hook, they run in discovery order. The first rejection stops remaining hooks.

Post-hooks are fire-and-forget. Uncaught exceptions are logged but don't crash the extension.

## Hook Selection Guidelines

- Use pre-hooks for validation, post-hooks for side effects.
- Avoid heavy work (network calls, spawning processes) in pre-hooks. Keep them fast.
- Do not mutate state in pre-hooks. They are for gating only.
- Register the narrowest hook possible. Use `issue:status:changing` rather than `issue:updating` when you only care about status transitions.
