Skip to main content
Lifecycle hooks are experimental and subject to change. Multi-level hooks (swarm and identity) are new — the API may evolve in future releases.
Hooks are shell scripts that run at specific points during deployment and provisioning. They let you automate secret resolution, server setup, and pre-launch configuration without modifying the core deployment pipeline. Hooks are available at three levels — swarm, identity, and plugin — so you can scope automation to exactly where it belongs.

Three Levels

LevelDefined InScopeExample Use
Swarmclawup.yamlAll agents in the deploymentInstall monitoring, register team webhook
Identityidentity.yamlAgents using this identityRole-specific tools, API setup
PluginPlugin manifestPer-pluginResolve derived secrets, configure integrations

Swarm-Level Hooks

Defined in clawup.yaml, swarm hooks run for every agent in your deployment. Use them for fleet-wide concerns.
# clawup.yaml
stackName: prod
provider: hetzner
region: fsn1
instanceType: cx32
ownerName: Jane Doe

hooks:
  postProvision: |
    echo "Installing fleet monitoring agent..."
    curl -sSL https://get.datadoghq.com/agent | sh
  resolve:
    SHARED_WEBHOOK_URL: |
      curl -s https://api.example.com/webhook-url \
        -H "Authorization: Bearer $ADMIN_TOKEN" \
        | jq -r ".url"

agents:
  - name: agent-eng
    identity: ./eng
    # ...

Identity-Level Hooks

Defined in identity.yaml, identity hooks run for all agents using that identity. Use them for role-specific setup.
# identity.yaml
name: eng
displayName: Titus
role: eng
emoji: building_construction
description: Lead engineering, coding, shipping
volumeSize: 50

hooks:
  postProvision: |
    echo "Installing engineering-specific tools..."
    npm install -g turbo
  preStart: |
    echo "Verifying repo access..."
    gh auth status

skills:
  - eng-queue-handler
templateVars:
  - OWNER_NAME

Plugin-Level Hooks

Defined in a plugin’s manifest, plugin hooks run for that plugin only. Resolve hooks at this level must match an autoResolvable secret key in the plugin manifest.
# Plugin manifest (e.g., openclaw-linear)
secrets:
  apiKey:
    envVar: LINEAR_API_KEY
    autoResolvable: false
  linearUserUuid:
    envVar: LINEAR_USER_UUID
    autoResolvable: true

hooks:
  resolve:
    linearUserUuid: |
      curl -s -X POST https://api.linear.app/graphql \
        -H "Authorization: $LINEAR_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"query":"{ viewer { id } }"}' \
        | jq -r ".data.viewer.id"
  postProvision: |
    echo "Installing Linear CLI..."
    deno install --global jsr:@schpet/linear-cli

Execution Order

Lifecycle Hooks (postProvision, preStart)

All levels run sequentially, broadest first:
swarm → identity → plugin
For a deployment with two agents using different identities and plugins:
Agent A:
  1. swarm postProvision
  2. identity:eng postProvision
  3. plugin:openclaw-linear postProvision
  4. plugin:slack postProvision

Agent B:
  1. swarm postProvision
  2. identity:researcher postProvision
  3. plugin:slack postProvision

Resolve Hooks

Resolve hooks use most-specific-wins on key conflicts:
plugin > identity > swarm
If the same environment variable key is defined at multiple levels, the most specific definition wins. Plugin-level resolve keys must match an autoResolvable: true secret in the plugin manifest. Swarm and identity-level resolve keys are environment variable names directly.

Onboard Hooks

Onboard hooks run sequentially, broadest first. Swarm onboard runs once, then per agent: identity onboard followed by plugin onboards.
1. swarm onboard (once)
2. Per agent:
   a. identity onboard
   b. plugin onboard(s)

Execution Timeline

The following shows where each hook type fires during the deployment lifecycle:
clawup deploy (your machine)

├─ Load clawup.yaml + .env
├─ ▶ resolve hooks ◀──── swarm → identity → plugin (most-specific-wins per key)
├─ ▶ onboard hooks ◀──── swarm (once) → per-agent: identity → plugin
├─ Run Pulumi (provision infrastructure)

└─ Cloud-init (agent server)

   ├─ Install system packages, Node.js, Docker
   ├─ Install coding agent (Claude Code)
   ├─ Write environment variables
   ├─ ▶ postProvision hooks ◀── deps (root → ubuntu) → swarm → identity → plugin
   ├─ Inject workspace files (SOUL.md, etc.)
   ├─ Install plugins, configure OpenClaw
   ├─ ▶ preStart hooks ◀────── swarm → identity → plugin
   ├─ Start Tailscale proxy
   └─ Start OpenClaw gateway
Deps run as postProvision hooks. When an identity declares deps (e.g., gh, brave-search), their install and post-install scripts are prepended to the postProvision hooks array. Install scripts run as root (for apt-get, etc.), post-install scripts run as ubuntu (for auth/config). They execute before swarm, identity, and plugin hooks.

Hook Types

Clawup supports four hook types, each running at a different stage:
HookWhenWhereUse Case
resolveDuring clawup deployYour machine (CLI)Auto-resolve derived secrets via API calls
onboardDuring clawup onboardYour machine (CLI)Interactive first-time setup
postProvisionDuring cloud-initAgent serverInstall tools after base provisioning
preStartDuring cloud-initAgent serverFinal config before gateway starts
All four hook types are available at all three levels (swarm, identity, plugin).

Resolve Hooks

Resolve hooks auto-derive secret values from other secrets. They run on your machine during clawup deploy, before infrastructure is provisioned. When: During deploy, after .env is loaded Input: Environment variables (including other secrets already in .env) Output: stdout is captured as the resolved value (must be non-empty) Timeout: 30 seconds

How It Works

Each resolve hook is a shell script keyed by a secret name. At the plugin level, the key must match a secret marked autoResolvable: true in the plugin manifest. At the swarm and identity levels, keys are environment variable names directly. During deploy, Clawup runs each script and stores stdout as the secret’s value. If the same key appears at multiple levels, the most specific definition wins (plugin > identity > swarm).

Example: Plugin-Level Resolve (Linear User UUID)

The built-in openclaw-linear plugin resolves the Linear user UUID from the API key:
# Plugin manifest
secrets:
  apiKey:
    envVar: LINEAR_API_KEY
    scope: agent
    isSecret: true
    required: true
    autoResolvable: false
  linearUserUuid:
    envVar: LINEAR_USER_UUID
    scope: agent
    isSecret: false
    required: false
    autoResolvable: true

hooks:
  resolve:
    linearUserUuid: |
      curl -s -X POST https://api.linear.app/graphql \
        -H "Authorization: $LINEAR_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"query":"{ viewer { id } }"}' \
        | jq -r ".data.viewer.id"

Example: Identity-Level Resolve (Notion Workspace ID)

An engineering identity that resolves a workspace ID for all agents with this role:
# identity.yaml
hooks:
  resolve:
    NOTION_WORKSPACE_ID: |
      curl -s https://api.notion.com/v1/users/me \
        -H "Authorization: Bearer $NOTION_API_KEY" \
        -H "Notion-Version: 2022-06-28" \
        | jq -r ".bot.workspace_name"

Example: Swarm-Level Resolve (Shared Webhook URL)

A swarm-level resolve hook that fetches a shared value for all agents:
# clawup.yaml
hooks:
  resolve:
    DEPLOY_WEBHOOK_URL: |
      curl -s https://api.example.com/deployments/webhook \
        -H "Authorization: Bearer $ADMIN_TOKEN" \
        | jq -r ".webhookUrl"

Rules

  • Plugin level: The resolve hook key must match a secret key with autoResolvable: true in the plugin manifest
  • Swarm/identity level: The resolve hook key is the environment variable name directly
  • The script must print exactly one value to stdout (trailing whitespace is trimmed)
  • If the script exits non-zero or prints nothing, the deploy fails with an error
  • All environment variables from .env are available in the script

Onboard Hooks

Onboard hooks run interactive first-time setup — like registering webhook URLs or configuring external services that require post-deploy information. When: During clawup onboard or clawup deploy --onboard Input: User-provided values (prompted interactively) + existing secrets Output: stdout is displayed as follow-up instructions Timeout: 120 seconds

How It Works

  1. Clawup checks each level (swarm, identity, plugin) for an onboard hook definition
  2. For each input defined in inputs, the user is prompted (or the value is read from .env)
  3. The hook script runs with all inputs and existing secrets as environment variables
  4. stdout is displayed as follow-up instructions to the user

Example: Webhook Registration

hooks:
  onboard:
    description: "Register webhook endpoint with external service"
    inputs:
      configToken:
        envVar: MY_CONFIG_TOKEN
        prompt: "Enter your configuration token"
        instructions: "Generate a token at https://example.com/settings/tokens"
        validator: "tok_"
    script: |
      WEBHOOK_URL="https://$(hostname).tail1234.ts.net/hooks/my-plugin"
      curl -s -X POST https://api.example.com/webhooks \
        -H "Authorization: Bearer $MY_CONFIG_TOKEN" \
        -d "{\"url\": \"$WEBHOOK_URL\"}"
      echo "Webhook registered at $WEBHOOK_URL"
    runOnce: true

Options

FieldTypeDefaultDescription
descriptionstringHuman-readable description shown before the hook runs
inputsobject{}Input values the user must provide (see below)
scriptstringShell script to execute
runOncebooleanfalseSkip when all required secrets are already configured
Each input in inputs supports:
FieldTypeDefaultDescription
envVarstringEnvironment variable name passed to the script
promptstringPrompt text shown to the user
instructionsstringHow to obtain this value
validatorstringRequired prefix (e.g., "tok_")
For more on running onboard hooks, see the clawup onboard CLI reference.

PostProvision Hooks

PostProvision hooks run on the agent server during cloud-init, after base packages (Node.js, Docker, coding agent) are installed but before workspace files are injected. When: Cloud-init, after Node.js/Docker/coding agent installation Where: Agent server, as the ubuntu user (dep install scripts run as root) Output: Streamed to cloud-init log (/var/log/cloud-init-output.log)

Use Cases

  • Installing custom system tools or CLI utilities
  • Downloading ML models or large data files
  • Setting up database clients or drivers
  • Configuring system-level settings
  • Fleet-wide monitoring (swarm level)
  • Role-specific tooling (identity level)

Example: Swarm-Level PostProvision

# clawup.yaml
hooks:
  postProvision: |
    echo "Installing fleet monitoring..."
    curl -sSL https://get.datadoghq.com/agent | sh

Example: Identity-Level PostProvision

# identity.yaml
hooks:
  postProvision: |
    echo "Installing engineering tools..."
    npm install -g turbo prettier

Example: Plugin-Level PostProvision

# Plugin manifest
hooks:
  postProvision: |
    echo "Installing analytics CLI..."
    curl -sSL https://get.analytics.example.com/cli | sh

How Deps Integrate with PostProvision

When an identity declares deps (e.g., gh, brave-search), those deps are automatically converted into postProvision hooks that run before swarm, identity, and plugin hooks. Each dep can produce up to two hooks:
  1. Install hook (dep:<name>:install) — runs as root for system-level installation (e.g., apt-get)
  2. Post-install hook (dep:<name>:post) — runs as ubuntu for authentication and configuration
For example, an identity with deps: [gh, brave-search] and a swarm-level postProvision hook produces this execution order:
postProvision hooks (in order):
  1. dep:gh:install          (as root)    ← apt-get install gh
  2. dep:gh:post             (as ubuntu)  ← gh auth login
  3. dep:brave-search:post   (as ubuntu)  ← openclaw config set tools.web.search
  4. swarm hook              (as ubuntu)  ← your clawup.yaml postProvision
  5. identity hook           (as ubuntu)  ← your identity.yaml postProvision
  6. plugin hooks            (as ubuntu)  ← plugin manifest postProvision
This means identity and plugin hooks can safely depend on deps being installed. For example, an identity preStart hook can use gh:
# identity.yaml
deps:
  - gh

hooks:
  preStart: |
    echo "Cloning working repo..."
    cd /home/ubuntu && gh repo clone $GITHUB_REPO workspace-repo

Environment

PostProvision hooks have access to:
  • Environment variables from .profile (API keys, tokens)
  • System tools installed in earlier phases (Node.js, npm, Docker)
  • Network access (Tailscale is connected at this point)
  • Deps installed earlier in the postProvision phase (e.g., gh CLI)

PreStart Hooks

PreStart hooks run on the agent server during cloud-init, after workspace files are injected and OpenClaw is configured, but before the gateway starts. When: Cloud-init, after workspace files + openclaw config set commands Where: Agent server, as the ubuntu user Output: Streamed to cloud-init log (/var/log/cloud-init-output.log)

Use Cases

  • Reading workspace config to generate derived files
  • Running database migrations
  • Pre-warming caches or indexes
  • Validating the full configuration before launch

Example: Generating a Config File from Workspace Data

hooks:
  preStart: |
    echo "Generating derived config from workspace files..."
    TEAM=$(grep -oP 'team:\s*\K.*' /home/ubuntu/.openclaw/workspace/IDENTITY.md)
    echo "{\"team\": \"$TEAM\"}" > /home/ubuntu/.config/my-plugin/config.json
    echo "Config generated for team: $TEAM"

Example: Pre-warming a Search Index

hooks:
  preStart: |
    echo "Building search index..."
    cd /home/ubuntu/.openclaw
    node -e "require('./build-index.js').run()" 2>&1
    echo "Search index ready"

Environment

PreStart hooks have access to everything postProvision hooks have, plus:
  • Workspace files in /home/ubuntu/.openclaw/workspace/
  • OpenClaw configuration (set via openclaw config set)
  • Installed plugins

Complete Examples

Swarm-Level: Fleet Monitoring

# clawup.yaml
stackName: prod
provider: hetzner
region: fsn1
instanceType: cx32
ownerName: Jane Doe

hooks:
  resolve:
    DATADOG_SITE: |
      echo "datadoghq.com"
  postProvision: |
    echo "Installing Datadog agent..."
    DD_API_KEY=$DATADOG_API_KEY DD_SITE=$DATADOG_SITE \
      bash -c "$(curl -sSL https://install.datadoghq.com/scripts/install_script.sh)"
  preStart: |
    echo "Tagging agent in Datadog..."
    datadog-agent config set tags "stack:prod,agent:$(hostname)"

agents:
  - name: agent-eng
    identity: ./eng
    volumeSize: 50

Identity-Level: Engineering Setup

# identity.yaml
name: eng
displayName: Titus
role: eng
emoji: building_construction
description: Lead engineering, coding, shipping
volumeSize: 50

hooks:
  resolve:
    SENTRY_PROJECT_SLUG: |
      curl -s https://sentry.io/api/0/projects/ \
        -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
        | jq -r '.[0].slug'
  postProvision: |
    echo "Installing engineering tools..."
    npm install -g turbo prettier
    pip install pre-commit
  preStart: |
    echo "Cloning working repo..."
    cd /home/ubuntu
    gh repo clone $GITHUB_REPO workspace-repo

deps:
  - gh
plugins:
  - openclaw-linear
  - slack
skills:
  - eng-queue-handler
templateVars:
  - OWNER_NAME
  - GITHUB_REPO

Plugin-Level: Full Analytics Plugin

# Plugin manifest
name: my-analytics
displayName: Analytics Dashboard
installable: true
needsFunnel: false
configPath: plugins.entries

secrets:
  apiKey:
    envVar: ANALYTICS_API_KEY
    scope: agent
    isSecret: true
    required: true
    autoResolvable: false
    validator: "ak_"
    instructions:
      title: "Analytics API Key"
      steps:
        - "1. Go to https://analytics.example.com/settings/api"
        - "2. Click 'Generate New Key'"
        - "3. Copy the key (starts with ak_)"
  projectId:
    envVar: ANALYTICS_PROJECT_ID
    scope: agent
    isSecret: false
    required: false
    autoResolvable: true

hooks:
  resolve:
    projectId: |
      curl -s https://api.analytics.example.com/v1/me \
        -H "Authorization: Bearer $ANALYTICS_API_KEY" \
        | jq -r ".defaultProjectId"
  onboard:
    description: "Register analytics event webhook"
    inputs:
      adminToken:
        envVar: ANALYTICS_ADMIN_TOKEN
        prompt: "Enter your admin token"
        validator: "adm_"
    script: |
      curl -s -X POST https://api.analytics.example.com/v1/webhooks \
        -H "Authorization: Bearer $ANALYTICS_ADMIN_TOKEN" \
        -d '{"events": ["deploy", "error"]}'
      echo "Analytics webhook registered"
    runOnce: true
  postProvision: |
    curl -sSL https://get.analytics.example.com/cli | sh
    analytics-cli --version
  preStart: |
    analytics-cli validate --config /home/ubuntu/.openclaw/config.json
    analytics-cli cache warm --project $ANALYTICS_PROJECT_ID

Tips and Best Practices

Make hooks idempotent. Deploys can be retried. Write hooks so that running them twice produces the same result — use mkdir -p instead of mkdir, check if files exist before downloading, etc. Handle errors gracefully. If a hook script exits with a non-zero code, the deploy fails. Use explicit error checks and helpful error messages:
curl -sSf https://example.com/install.sh | sh || {
  echo "ERROR: Failed to install tool. Check network connectivity." >&2
  exit 1
}
Keep hooks focused. Each hook should do one thing well. If you need complex setup, consider splitting across postProvision (system setup) and preStart (app config). Use the right level. Put fleet-wide concerns (monitoring, shared tooling) at the swarm level. Put role-specific setup (engineering tools, research APIs) at the identity level. Put plugin-specific automation (secret resolution, plugin CLI) at the plugin level. Test locally. You can test hook scripts locally before deploying:
# Test a resolve hook
LINEAR_API_KEY="lin_api_xxx" sh -c 'curl -s ...'

# Test a postProvision/preStart hook
sh -c '$(cat my-hook-script.sh)'
Use stderr for progress, stdout for values. In resolve and onboard hooks, stdout is captured as the result. Use >&2 to print progress messages that won’t interfere with the captured value:
echo "Fetching project ID..." >&2
curl -s https://api.example.com/me | jq -r ".projectId"