Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt

Use this file to discover all available pages before exploring further.

Lifecycle Hooks

Hooks let workspace admins run arbitrary logic when platform events fire — pre/post task delegation, agent start/stop, tool calls, LLM calls, memory writes, peer conversations, approvals, budget overruns, guardrail trips. They are the orchestration-layer equivalent of Claude Code’s shell hooks but generalised across three handler kinds: shell, http, and subagent.

Events

Defined in internal/hooks/types.go (15 events currently):
BucketEvents
Task delegationpre_task_delegation, post_task_delegation
Agent lifecyclepre_agent_start, post_agent_stop
Tool callspre_tool_call, post_tool_call
LLM callspre_llm_call, post_llm_call
Memory writespre_memory_write, post_memory_write
Peer conversationspre_peer_conversation, post_peer_conversation
Policyon_approval_requested, on_budget_exceeded, on_guardrail_triggered

Coverage status (as of PR #210)

Currently dispatched (call sites wired):
  • pre_agent_start, post_agent_stop — orchestrator agent lifecycle. Both payloads carry mission_id since PR #210 so a hook can scope its action to the running mission without a separate join.
  • on_approval_requested — fires when Harbor Master’s Gate returns Required=true, before the request lands in the approvals queue. The hook receives the same scope (workspace_id, crew_id, agent_id, mission_id), the credential ID, and the intent string. Use this to forward HITL notifications to Slack / PagerDuty / your own pager-of-choice. PR #210 finally wired this dispatch site — earlier docs claimed it fired but the call site was missing.
  • on_guardrail_triggered — fires from Lookout’s InputGuard on every finding (block, sanitize, AND log mode — log isn’t “guard disabled,” it’s observability + alerting without breaking the call). Wired via a GuardListener context callback in internal/pipeline/runner_llm.go so the integration path stays decoupled (lookout has no dep on the hooks package). Payload:
    {
      "direction": "input",
      "kind": "role_override",        // role_override | system_prompt_leak | jailbreak | zero_width_unicode | rtl_override_unicode | lakera_detected
      "detail": "instruction-override request",
      "matched": "ignore previous instructions",
      "pipeline_id": "...",
      "pipeline_run": "...",
      "step_id": "...",
      "agent_slug": "..."
    }
    
    Severity on the HookEventContext is the finding’s severity (info | warn | error) so a webhook can throttle by criticality. Dispatch failures are logged but never change the guard’s verdict — a blocked call stays blocked even if the alert channel is down.
Defined but not yet dispatched (add a hooks.Dispatch call at the relevant point when the corresponding platform path wires through):
  • pre_task_delegation, post_task_delegation — hook into the assignment path (orchestrator.RunAgentForAssignment).
  • pre_tool_call, post_tool_call — needs orchestrator tool-call interception.
  • pre_llm_call, post_llm_call — fits cleanly into llm.Middleware.
  • pre_memory_write, post_memory_write — memory consolidation path.
  • pre_peer_conversation, post_peer_conversation — peer bridge.
  • on_budget_exceeded — Paymaster already emits journal entries; hooks are the natural next step.
When adding a new dispatch site, also add a row to AllEvents in types.go and test coverage in hooks_test.go.

Handler kinds

const (
    HandlerKindShell    HandlerKind = "shell"
    HandlerKindHTTP     HandlerKind = "http"
    HandlerKindSubagent HandlerKind = "subagent"
)

shell

Runs a command via exec.CommandContext. Event context is passed as env vars (CREWSHIP_EVENT, CREWSHIP_AGENT_ID, CREWSHIP_MISSION_ID, CREWSHIP_PAYLOAD, …). Stdout is captured as Result.Payload. Shell hooks require OWNER role at registration time — an ADMIN cannot create a shell hook because arbitrary code execution on the host is a privilege escalation vector. The store enforces this via a register-time argument, not a row property:
const allowedShell = true // pass true only when the caller is confirmed OWNER
_, err := hooks.Register(ctx, db, h, allowedShell)
// returns hooks.ErrShellHookNotAllowed if h.HandlerKind == "shell" && !allowedShell
allowedShell is the trailing boolean argument to hooks.Register (internal/hooks/store.go:22); the hooks_config table has no allowed_shell column. Callers that resolve role lazily (HTTP handlers, admin CLI) pass true only after confirming the request originated from an OWNER session.

http

POSTs the event context as JSON to handler_config.url. Supports HMAC signing via handler_config.hmac_secret (header X-Crewship-Signature: sha256=<hex>). Timeout defaults to 5s.

subagent

Dispatches to an LLM subagent via the orchestrator. Not wired by default — the orchestrator registers a subagent handler at startup (RegisterSubagentHandler) that the dispatcher looks up. If a subagent hook fires without a handler registered, Dispatch returns ErrSubagentHandlerNotConfigured.

Blocking vs non-blocking

type Hook struct {
    ...
    Blocking bool
}
BlockingDispatcher behaviour
trueSequential, same goroutine. A OutcomeBlock result short-circuits with *BlockedError — the caller uses errors.As to recover and abort the triggering operation.
falseFire-and-forget goroutine with context.Background(). OutcomeBlock is downgraded to a hook.blocked journal entry for audit; the triggering operation proceeds.
Errors from blocking handlers are logged but do NOT short-circuit — a buggy webhook cannot wedge the platform. Only explicit OutcomeBlock blocks.

Matcher

All fields optional and AND-combined. Empty matcher matches every event.
FieldSemantics
toolsRegex list against tool name (tool events only). Any-match.
agent_idsExact match.
crew_idsExact match; layered on top of row-level crew_id.
severitiesExact match for severity-bearing events.
whenReserved for future CEL/expr predicate. Ignored today.

Registration

There is no runtime create/delete endpoint by design. Hooks are registered at config time via hooks.Register(ctx, db, Hook{...}), typically from a workspace seed or a dedicated provisioning script. The API surface is strictly read + enable/disable.
_, err := hooks.Register(ctx, db, hooks.Hook{
    WorkspaceID: ws,
    CrewID:      crew, // empty = workspace-wide
    Event:       hooks.EventOnApprovalRequested,
    Matcher:     hooks.Matcher{Severities: []string{"high", "critical"}},
    HandlerKind: hooks.HandlerKindHTTP,
    HandlerConfig: map[string]any{
        "url":    "https://hooks.slack.com/services/...",
        "method": "POST",
    },
    Blocking: false,
    Enabled:  true,
    CreatedBy: userID,
})

Endpoints

  • GET /api/v1/hooks[?crew_id=...] — list registered hooks (workspace-scoped).
  • POST /api/v1/hooks/{id}/enable — OWNER/ADMIN only.
  • POST /api/v1/hooks/{id}/disable — OWNER/ADMIN only.
Toggling emits system.hook_toggled with the actor’s user ID so the audit trail captures who flipped which hook when. See Hooks API for schemas.

CLI

crewship hooks list
crewship hooks list --crew backend-team
crewship hooks enable hk_abc
crewship hooks disable hk_abc
Full reference: crewship hooks. No register subcommand — config-time only.

Journal entries

  • hook.fired — every dispatch lands this. Severity escalates to warn on non-pass outcomes.
  • hook.blocked — separate entry so UI filters for “what blocked” don’t have to parse payloads.
  • system.hook_toggled — admin enabled/disabled a hook.
Payload includes hook_id, handler_kind, outcome, latency_ms, blocking, and the handler’s response when non-empty.

Gotchas

  • Shell hooks bypass container isolation. They run on the host with the privileges of the crewship process. Restrict to OWNER role and audit handler_config.command carefully.
  • Non-blocking goroutines use context.Background(). Cancelling the request that triggered the dispatch does NOT cancel the hook; per-handler timeouts bound runtime. If a webhook hangs, the goroutine times out but is not killable from the caller.
  • Event names are stable. Renaming requires a migration on hooks_config.event — breaks every existing registration.
  • Matcher regex compile is cached forever. A bad regex is cached as a nil sentinel; subsequent calls skip it silently. If a hook never fires, check the logs for hooks: compile regex failed.