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
| Level | Defined In | Scope | Example Use |
|---|
| Swarm | clawup.yaml | All agents in the deployment | Install monitoring, register team webhook |
| Identity | identity.yaml | Agents using this identity | Role-specific tools, API setup |
| Plugin | Plugin manifest | Per-plugin | Resolve 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:
| Hook | When | Where | Use Case |
|---|
resolve | During clawup deploy | Your machine (CLI) | Auto-resolve derived secrets via API calls |
onboard | During clawup onboard | Your machine (CLI) | Interactive first-time setup |
postProvision | During cloud-init | Agent server | Install tools after base provisioning |
preStart | During cloud-init | Agent server | Final 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
- Clawup checks each level (swarm, identity, plugin) for an
onboard hook definition
- For each input defined in
inputs, the user is prompted (or the value is read from .env)
- The hook script runs with all inputs and existing secrets as environment variables
- 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
| Field | Type | Default | Description |
|---|
description | string | — | Human-readable description shown before the hook runs |
inputs | object | {} | Input values the user must provide (see below) |
script | string | — | Shell script to execute |
runOnce | boolean | false | Skip when all required secrets are already configured |
Each input in inputs supports:
| Field | Type | Default | Description |
|---|
envVar | string | — | Environment variable name passed to the script |
prompt | string | — | Prompt text shown to the user |
instructions | string | — | How to obtain this value |
validator | string | — | Required 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:
- Install hook (
dep:<name>:install) — runs as root for system-level installation (e.g., apt-get)
- 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"