Skip to main content
The lifecycle hook registry. Hooks fire shell commands, HTTP webhooks, or subagents on lifecycle events; this API registers them, edits them, removes them, and toggles them on or off. See the Hooks guide.
All endpoints require authentication and are workspace-scoped. Every write (create / update / delete / enable / disable) requires OWNER or ADMIN. Creating or editing a shell handler additionally requires OWNER — see the shell gate.

Endpoints


Reading hooks

List the registered hooks for a workspace, optionally narrowed to one crew.

List hooks

Query parameters: Response: 200 OK
Errors:

Writing hooks

Create a hook

Auth: OWNER or ADMIN. handler_kind: "shell" requires OWNER. Body:
Response: 201 Created, body is the created hook in the same shape list returns per row. Errors: Writes an audit_logs row with action hook.create.
A hook whose event is misspelled would insert cleanly (the hooks_config schema constrains handler_kind, not event), never match the dispatcher’s query, and silently never fire — while still listing and toggling like a healthy hook. That is why an unknown event is a hard 400 rather than a warning.

Update a hook

Auth: OWNER or ADMIN. OWNER is required to set handler_kind: "shell" or to edit a hook that is already shell. Semantics are patch, not put: only the fields present in the body change. An omitted field keeps its current value, so {"event": "post_tool_call"} does not clear the matcher, the blocking flag, or the enabled flag. Accepts the same fields as create, all optional. Response: 200 OK with the updated hook. Errors: as create, plus 404 when the hook is outside your workspace (cross-tenant IDs are indistinguishable from missing ones). Writes an audit_logs row with action hook.update.

Delete a hook

Auth: OWNER or ADMIN. The request has no body. Response: 200 OK
Errors: 401 unauthenticated, 403 for a non-OWNER/ADMIN, 404 when the hook is not in your workspace. Deletion is permanent. To stop a hook firing while keeping its definition, use disable instead. Writes an audit_logs row with action hook.delete carrying the deleted hook’s event, handler kind, and crew scope — after the delete there is nothing left to reconstruct it from.

The shell gate

handler_kind: "shell" runs a command on the crewshipd host, outside container isolation, with the privileges of the crewship process. That is a strictly larger grant than the rest of the ADMIN surface, so it needs OWNER — on create and on update alike. An http hook PATCHed into a shell hook is the same grant by a longer road, and is refused for the same reason. The gate is enforced twice on purpose: in the handler (which knows the caller’s role) and again in hooks.Register / hooks.Update (which is the only write path into hooks_config). Either alone would be sufficient; both means a future call site that forgets the check still cannot land a shell hook.

Toggling hooks

Flip a hook on or off. Both transitions emit system.hook_toggled into the journal with the actor’s user ID.
Enabling a hook can invoke shell commands, hit third-party webhooks, or dispatch subagents — which is why both endpoints require OWNER or ADMIN.

Enable

Path parameters: id is the hook ID. The request has no body. Auth: OWNER or ADMIN only (403 otherwise). Enabling a hook can invoke shell commands, hit third-party webhooks, or dispatch subagents — the role bar matches that risk. Response: 200 OK
Errors: Emits system.hook_toggled into the journal with the actor’s user ID.

Disable

Path parameters: id is the hook ID. The request has no body. Same auth rules, same response shape with "enabled": false. Errors: 401 when unauthenticated, 403 for a non-OWNER/ADMIN, 404 when the hook is outside the workspace, and 500 on a database error.

Registration from Go

In-process provisioning code can skip HTTP and call the store directly. This is the same function the endpoints above call — there is one write path, not two:
The trailing allowedShell bool argument is the gate on HandlerKindShell: pass false for any non-OWNER context — the function returns hooks.ErrShellHookNotAllowed if the hook is a shell kind and this flag is false. HTTP / subagent hooks are unaffected by the flag. Register also returns hooks.ErrUnknownEvent for an event outside the declared fifteen. hooks.Update(ctx, db, workspaceID, hook, allowedShell) is the equivalent for edits. It takes the full desired hook (not a patch — merging is the HTTP layer’s job), pins id + workspace_id from its arguments so a struct naming another workspace cannot move the row, and never rewrites created_at / created_by.