> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clawup.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Lifecycle Hooks

> Run shell scripts at deploy and provision time — at the swarm, identity, or plugin level

<Warning>
  Lifecycle hooks are **experimental** and subject to change. Multi-level hooks (swarm and identity) are new — the API may evolve in future releases.
</Warning>

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.

```yaml theme={null}
# 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.

```yaml theme={null}
# 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.

```yaml theme={null}
# 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
```

<Note>
  **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.
</Note>

## 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:

```yaml theme={null}
# 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:

```yaml theme={null}
# 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:

```yaml theme={null}
# 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

```yaml theme={null}
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](/cli/onboard).

## 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

```yaml theme={null}
# clawup.yaml
hooks:
  postProvision: |
    echo "Installing fleet monitoring..."
    curl -sSL https://get.datadoghq.com/agent | sh
```

### Example: Identity-Level PostProvision

```yaml theme={null}
# identity.yaml
hooks:
  postProvision: |
    echo "Installing engineering tools..."
    npm install -g turbo prettier
```

### Example: Plugin-Level PostProvision

```yaml theme={null}
# 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`:

```yaml theme={null}
# 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

```yaml theme={null}
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

```yaml theme={null}
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

```yaml theme={null}
# 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

```yaml theme={null}
# 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

```yaml theme={null}
# 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:

```bash theme={null}
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:

```bash theme={null}
# 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:

```bash theme={null}
echo "Fetching project ID..." >&2
curl -s https://api.example.com/me | jq -r ".projectId"
```
