---
title: Event Hooks
description: User guide for repo and personal event hooks, triggers, runners, program payloads, outputs, and testing.
relatedCommands:
  - alpaca event-hook --help
  - alpaca event-hook config --help
  - alpaca event-hook add --help
  - alpaca event-hook create --help
  - alpaca event-hook update --help
  - alpaca event-hook test --help
relatedDocs:
  - config
  - command-flows
  - publishing
---

# Event Hooks

Event hooks let AlpacaLoop call a user-provided hook after it successfully publishes tracker changes such as issue comments, labels, workflow state comments, or target closure. Use them for notifications, audit records, and lightweight integrations that should run after AlpacaLoop has made a visible update.

Hooks are best for side effects outside the tracker. They should not modify tracker labels, comments, issue state, pull requests, files, or workflow state unless that is the explicit purpose of a carefully reviewed hook.

## When to Use

- Use repo-scoped hooks for behavior the whole repository should share.
- Use user-scoped hooks for personal machine-local behavior such as private Slack notifications.
- Use Markdown hooks when an AI worker should interpret instructions and perform the action.
- Use program hooks when the behavior should be deterministic, fast, and easy to test.
- Use `alpaca event-hook test` only when a real side effect is acceptable or the hook is pointed at a sandbox target.

## Key Commands

```bash
alpaca event-hook -h
alpaca event-hook config -h
alpaca event-hook add -h
alpaca event-hook create -h
alpaca event-hook update -h
alpaca event-hook test -h
```

## Scopes

Repo-scoped hooks are stored in `alpaca.config.json` and use repository-relative hook paths. User-scoped hook registrations are stored machine-locally, and hook files must live under the configured user event-hook directory.

| Scope | Where the registration lives | Hook file path rule | Typical use |
| --- | --- | --- | --- |
| `repo` | `alpaca.config.json` | Repository-relative path. | Shared team automation checked into the repo. |
| `user` with `--cwd <path>` | Machine-local project registry. | Relative to the configured user event-hook directory. | Personal hook for one tracked repo. |
| `user` with `--all-repos` | Machine-local global registry. | Relative to the configured user event-hook directory. | Personal hook for every tracked repo. |

Before using user-scoped hook files, set the visible personal hook directory with `alpaca event-hook config set --user-dir <path>`. Hidden `~/.alpaca/event-hooks/.../hooks.json` files are registries, not hook file directories.

When multiple hooks match the same event, AlpacaLoop resolves and runs them in this order: repo hooks, global user hooks (`user:all`), then project user hooks (`user:project`). Duplicate resolved hook file paths are rejected before any hook runs.

## Hook Registration

Each hook has a path, a trigger list, and optionally a runner.

| Field | Where it appears | Meaning |
| --- | --- | --- |
| `path` | `alpaca.config.json` or user hook registry. | Hook file path. Repo hooks use repository-relative paths. User hooks use paths relative to the user event-hook directory. |
| `on` | `alpaca.config.json` or user hook registry. | Trigger tokens that decide when the hook runs. Values can be repeated or comma-separated in CLI commands. |
| `runner` | Optional in config and CLI. | Runner command. When omitted, AlpacaLoop infers the runner from the file extension. |

Use `alpaca event-hook add` when the hook file already exists. Use `alpaca event-hook create` when a worker should draft a new hook file from a prompt. Use `alpaca event-hook update` when a registered hook file should be changed from a prompt.

`event-hook create --save-draft` and `event-hook update --save-draft` write reusable draft JSON and do not write hook files or registrations. Apply reviewed drafts with `--apply-draft <path|->` so the reviewed content is preserved.

Prompt-mode event hook authoring uses the packaged `event-hook-create` system prompt together with `worker-common`. Use the packaged default for ordinary hook drafts. If a repository needs durable hook-writing conventions, run `alpaca eject system-prompts`, edit `.alpaca/system-prompts/event-hook-create.md`, then publish that prompt path when service automation should use it. Keep hook destination details, secrets, and one-off trigger choices in the command prompt, hook file, environment, or registration rather than in the system prompt.

| Command | Main options | Behavior |
| --- | --- | --- |
| `event-hook config show` | none | Shows the configured user event-hook directory. |
| `event-hook config set` | `--user-dir <path>` | Creates and stores the user event-hook directory in machine-local settings. |
| `event-hook add` | `--scope repo\|user`, `--path`, `--on`, optional `--runner`, optional `--cwd` or `--all-repos` for user scope. | Registers an existing hook file. |
| `event-hook list` | optional `--scope repo\|user\|all`, optional `--cwd`, optional `--all-repos`. | Lists matching repo and/or user registrations. |
| `event-hook remove` | `--scope repo\|user`, `--path`, optional `--cwd` or `--all-repos` for user scope. | Removes a matching registration. It does not delete the hook file. |
| `event-hook create` | `--scope repo\|user`, `--on`, `--prompt`, optional `--path`, optional `--runner`, optional `--save-draft`. | Drafts or writes a new hook file and registration. |
| `event-hook update` | `--scope repo\|user`, `--path`, `--prompt`, optional `--save-draft`. | Updates a registered hook file body. It does not change path, runner, or triggers. |
| `event-hook test` | `--path`, optional `--scope repo\|user`, optional `--runner`. | Runs one hook with the built-in sample event. |

## Triggers

`--on <trigger>` accepts workflow label keys and built-in trigger tokens.

| Trigger kind | Examples | When it matches |
| --- | --- | --- |
| Workflow label key | `aiNeedsHuman`, `aiCoolingDown`, `aiWorkingInPr` | The published transition includes the corresponding workflow label state. |
| `completed` | `completed` | AlpacaLoop publishes a workflow completion or closes the target issue. |
| `issueCreated` | `issueCreated` | AlpacaLoop publishes a newly created issue. |
| `workerFailed` | `workerFailed` | A worker failure is handed off through the tracker. |
| `systemFailed` | `systemFailed` | A control-plane failure is handed off through the tracker. |
| `all` | `all` | Any successful publish event. |

Hooks run only after AlpacaLoop successfully publishes tracker changes. If the publish does not happen, hooks do not run.

Workflow label triggers match labels added by the publish, not labels that were already present. New issue publishes can match label triggers from the created issue labels. Existing issue publishes without a workflow projection do not match workflow label triggers. When a cooldown publish adds both `aiScheduled` and `aiCoolingDown`, AlpacaLoop reports only `aiCoolingDown` so hooks do not double-handle the same cooldown event.

## Runners

Runner selection defaults from the hook file extension. Markdown files run through a worker hook runner. JavaScript files use Node, Python files use `python3`, and shell files use `bash`. A custom runner can be supplied with `--runner`.

| Extension | Default runner | Input | Expected output |
| --- | --- | --- | --- |
| `.md` | `worker` | Hook Markdown plus event context passed to the configured worker runtime. | Worker event hook result. |
| `.js`, `.mjs`, `.cjs` | `node` | Event JSON on stdin. | JSON result on stdout. |
| `.py` | `python3` | Event JSON on stdin. | JSON result on stdout. |
| `.sh` | `bash` | Event JSON on stdin. | JSON result on stdout. |

Program hooks are supported on Linux, macOS, and WSL2. Native Windows execution is not a v1 target.

For program hooks, AlpacaLoop starts the runner from the repository root and passes the resolved hook file path as the program file. The event payload is not passed as CLI arguments; it is written to stdin as one JSON object followed by a newline. Program hooks have a 60 second timeout.

## Program Hook Payload

Program hooks receive one JSON object on stdin. The payload shape is:

| Field | Type | Meaning |
| --- | --- | --- |
| `schemaVersion` | `1` | Payload schema version. |
| `eventId` | `string` | Stable id for this published event and target state. |
| `occurredAt` | ISO timestamp string | Time AlpacaLoop built the event payload. |
| `hook.path` | `string` | Registered hook path. |
| `hook.runner` | `string` | Resolved runner name. |
| `matchedTriggers` | `string[]` | Hook triggers that matched this event. Use this when one hook handles multiple triggers. |
| `repository.host` | `string` | Repository provider kind such as `github`, `gitea`, or `gitlab`. |
| `repository.projectRef` | `string` | Repository project reference from config. |
| `repository.baseBranch` | `string` | Repository base branch from config. |
| `issue.id` | `string` | Tracker issue id. Use this for provider APIs. |
| `issue.displayId` | `string` | Human-facing issue id such as `#123`. |
| `issue.title` | `string` | Current issue title. |
| `issue.url` | `string` | Tracker URL for the target issue or pull request thread target. |
| `issue.kind` | `string` | Target kind reported by the tracker. |
| `issue.isOpen` | `boolean` | Whether the target is open after the publish. |
| `issue.labelNames` | `string[]` | Label names expected on the target after the publish. |
| `transition.previousStatus` | `string \| null` | Workflow status before the publish, when known. |
| `transition.nextStatus` | `string \| null` | Workflow status after the publish, when known. |
| `transition.previousLabel` | `string \| null` | Workflow label name before the publish, when known. |
| `transition.nextLabel` | `string \| null` | Workflow label name after the publish, when known. |
| `publish.status` | `"applied"` | Publish status. Event hooks only receive applied publishes. |
| `publish.visibleEvent` | `string \| null` | User-visible workflow event name, when the publish has one. |
| `publish.targetClosed` | `boolean` | Whether the publish closed the target. |
| `publish.commentUrl` | `string \| null` | URL of the visible update comment, when one was published. |
| `publish.workflowStateCommentUrl` | `string \| null` | URL of the workflow state comment, when one was published. |
| `pullRequest.url` | `string \| null` | Pull request URL when the target is a pull request thread. |
| `pullRequest.threadId` | `string \| null` | Pull request thread id when the target is a pull request thread. |
| `result.runMode` | `string \| null` | Worker run mode that produced the publish, when available. |
| `result.workerOutcome` | `string \| null` | Worker outcome such as `proceed` or `wait_human`, when available. |
| `result.needsHumanReason` | `string \| null` | Machine-readable reason for human handoff, when available. |
| `result.summary` | `string \| null` | Short worker or control-plane summary, when available. |
| `result.nextStep` | `string \| null` | Suggested next human action, when available. |
| `result.workflowTemplateName` | `string \| null` | Workflow template name, when available. |

Program hooks should tolerate unknown future fields and should not require nullable fields to be present.

Failure handoff payloads avoid leaking raw worker or command stderr. For `workerFailed` and `systemFailed`, AlpacaLoop uses the public visible update text when available. If no safe visible update exists, failure `summary` and `nextStep` are set to `null` instead of falling back to raw internal failure text.

## Program Hook Output

Program hooks must write exactly one JSON result to stdout. Stderr is captured in hook logs and may be used for diagnostics.

| Field | Type | Meaning |
| --- | --- | --- |
| `status` | `"completed" \| "skipped" \| "failed"` | Hook result status. |
| `summary` | `string` | Short human-readable summary for logs and command output. |

Use `skipped` when the hook intentionally did nothing. Use `failed` when the hook could not complete its intended side effect.

Example:

```json
{"status":"completed","summary":"Sent Slack notification."}
```

Do not print progress logs to stdout from program hooks. Extra stdout before or after the JSON result makes the hook result invalid. Write diagnostics to stderr instead.

## Logs and Duplicate Handling

AlpacaLoop writes event hook results to the issue log. Hook setup failures, runner failures, invalid stdout JSON, and captured stderr are logged there. Hook failures do not roll back tracker changes that were already published, and AlpacaLoop continues to later hooks when it can.

Before running a hook from a real publication, AlpacaLoop writes a machine-local ledger entry for the pair of event id and resolved hook path. If the same event and hook path are seen again, AlpacaLoop skips the duplicate. `event-hook test` runs a single hook directly and does not use this publication ledger.

## Testing

`event-hook test` resolves the path from the selected scope and runs the hook with a built-in sample event. The sample event matches `completed` and `all`; it does not let the caller choose an arbitrary trigger or payload.

| Command option | Meaning |
| --- | --- |
| `--scope repo\|user` | Resolve the hook path from the repository root or configured user directory. Defaults to `repo`. |
| `--path <path>` | Hook file path to test. Paths must stay inside the selected scope. |
| `--runner <command>` | Override the runner. Defaults from the file extension. |

Because `event-hook test` may perform real side effects, point notification hooks at a sandbox destination or make the hook return `skipped` for the built-in `completed` sample when that is appropriate. For trigger-specific program hook tests, invoke the hook directly with a representative JSON payload on stdin and point external integrations at a local test server.

## Watch Outs

- `event-hook test` may perform real side effects. Use a harmless hook or a sandbox target when testing.
- User-scoped hook files must stay inside the configured user directory. AlpacaLoop rejects paths that escape that directory.
- Keep secrets out of hook files, config, issue text, and workflow templates. Read them from environment variables, keychains, or machine-local secret files.
- Program hooks should be idempotent where possible. A retry or rerun can otherwise duplicate external side effects.
- Hook failures are logged, but they do not roll back the tracker changes that already published.

## Related Docs

- `alpaca docs show config`
- `alpaca docs show command-flows`
- `alpaca docs show publishing`
