---
name: ppclaw
description: >
  Manage OpenClaw sandboxes on PPIO cloud via CLI or REST API.
  Use this skill whenever the user wants to launch, stop, pause, resume, diagnose, or configure an OpenClaw sandbox, manage gateway or node connections, pair messaging channels (Telegram, Discord, Feishu, etc.), set up web terminal or file manager, approve devices, configure on-demand idle timeout, export or import gateway config, install ClawHub skills,
  or when they mention PPClaw, OpenClaw.
argument-hint: "<command> [sandbox_id]"
---

# PPClaw

PPClaw launches OpenClaw instances on [PPIO Agent Sandbox](https://ppio.com/docs/sandbox/overview.md). It creates a cloud sandbox, configures OpenClaw with LLM providers and gateway auth, then exposes Web UI and WebSocket URLs.

## Onboarding

Walk first-time users through these steps:

1. **Install:**
   - macOS / Linux: `curl -fsSL https://ppclaw.ppio.com/install.sh | bash`
   - Windows (PowerShell): `irm https://ppclaw.ppio.com/install.ps1 | iex`
   - Or: `pipx install ppclaw`
2. **Get API key:** Sign up at <https://ppio.com/user/login>, then copy from <https://ppio.com/settings/key-management>.
3. **Configure:** `export PPIO_API_KEY=<key>` in shell profile (or `--api-key <key>` per command)
4. **Launch:** `ppclaw launch --json` (~60s, returns `sandbox_id`, `webui` URL, credentials)
5. **Verify:** `ppclaw list --json`
6. **Cleanup:** `ppclaw stop <sandbox_id> --json`

## Authentication

All operations require a PPIO API key.

**Resolution order:** `--api-key` flag > `PPIO_API_KEY` env var. Missing key → `MISSING_API_KEY` (exit 10).

**REST API:** `Authorization: Bearer <key>` header on all endpoints except `/health`.

## CLI Commands

All commands support `--json / -j`. JSON output is always one object on stdout with `"status": "ok"` or `"status": "error"`.

### Sandbox Lifecycle

| Command | Description |
| --- | --- |
| `ppclaw launch [--timeout 180] [--template-id ID] [--env KEY=VALUE] [--env-file PATH] [--type always-on\|on-demand] [--idle-timeout 300]` | Create sandbox (~60s). Returns `sandbox_id`, `webui`, `gateway_ws`, `gateway_token`, service URLs. Repeat `--env` to inject creation-time environment variables; use `--env-file` for secrets, with explicit `--env` values taking precedence. Injected values are never returned. `--template-id` selects a dedicated template for this launch. Default type is `always-on`. `on-demand` auto-pauses after idle timeout and auto-resumes on HTTP access. `--idle-timeout` only valid with `--type on-demand` (min 60, max 86400). `--type on-demand` cannot combine with `--mode node`. |
| `ppclaw list` | List active sandboxes: `{sandbox_id, state, mode, sandbox_type, cpu, memory_mb, started_at}`. |
| `ppclaw status <id>` | Sandbox state, URLs, credentials, `sandbox_type`, `idle_timeout`. Safe on paused sandboxes (does not trigger resume). Shows `paused_notice` when paused. |
| `ppclaw pause <id>` | Pause sandbox. State preserved, no charges. |
| `ppclaw resume <id>` | Resume paused sandbox (~1s). |
| `ppclaw stop <id>` | **Irreversible.** Destroys sandbox. Prefer `pause` to save costs. |

### Configuration (On-Demand)

| Command | Description |
| --- | --- |
| `ppclaw config set <id> idle-timeout <seconds>` | Update idle timeout (60–86400s). Takes effect on next agent check cycle. |
| `ppclaw config set <id> timeout <seconds>` | Update sandbox lifetime (300–2592000s). |

### Gateway

| Command | Description |
| --- | --- |
| `ppclaw gateway update <id>` | Update OpenClaw + restart gateway. |
| `ppclaw gateway restart <id>` | Restart gateway without upgrading. |
| `ppclaw gateway config export <id> [-o FILE]` | Export OpenClaw config (may contain API keys). |
| `ppclaw gateway config import <id> <FILE> [--no-restart] [--yes]` | Import config (full overwrite, restarts gateway by default). |

### Diagnostics

| Command | Description |
| --- | --- |
| `ppclaw doctor <id> [flags]` | Diagnose gateway. Flags: `--fix`, `--deep`, `--repair`, `--yes`, `--generate-gateway-token`, `--force`. |

### Pairing (telegram, discord, whatsapp, signal, slack)

| Command | Description |
| --- | --- |
| `ppclaw pair list <id> <channel>` | List pending pairing requests. |
| `ppclaw pair approve <id> <channel> <code>` | Approve a pairing request. |

### Feishu Channel Configuration

| Command | Description |
| --- | --- |
| `ppclaw pair feishu <id> --app-id <ID> --app-secret <SECRET> [--mode websocket\|webhook] [--domain feishu\|lark]` | Configure Feishu channel. Auto-installs the `@openclaw/feishu` plugin if missing (unbundled since OpenClaw 2026.5.29), so the first pair on a sandbox takes ~1 min. Default mode is `websocket` (connects out to Feishu, no public URL needed). `--domain` selects `feishu` (China, default) or `lark` (International). For `webhook` mode, `--verification-token` and `--encrypt-key` are required. Additional options: `--webhook-host` (default 0.0.0.0), `--webhook-port` (default 3000), `--webhook-path` (default /feishu/events), `--dm-policy` (default pairing). |

### Nodes

Create a bare sandbox with `--mode node`, then connect to an external gateway.

| Command | Description |
| --- | --- |
| `ppclaw launch --mode node [--display-name NAME]` | Node sandbox (services only, no gateway). |
| `ppclaw node connect <id> --gateway <wss://...> [--gateway-token T]` | Connect node to gateway. Polls for pairing. |
| `ppclaw node disconnect <id>` | Disconnect node; sandbox stays alive. |

### Devices (Gateway-Side)

| Command | Description |
| --- | --- |
| `ppclaw devices list <id>` | List paired and pending devices on a gateway. |
| `ppclaw devices approve <id> [--request-id ID]` | Approve pending devices (omit ID → approve all). |

### Services

| Command | Description |
| --- | --- |
| `ppclaw services setup <id>` | Install ttyd + gohttpserver. Idempotent. Returns `terminal_url`, `filemanager_url`, credentials. |

### Skills

| Command | Description |
| --- | --- |
| `ppclaw skill install <id> <skill_name>` | Install a ClawHub skill (extension) into a sandbox. |

### System

| Command | Description |
| --- | --- |
| `ppclaw update` | Self-update CLI. |

## REST API

Base URL: `https://ppclaw.ppio.com/api` — interactive docs at `/api/docs` (Swagger) or `/api/redoc`.

Every CLI command maps to a REST endpoint. Key patterns:

| Pattern | Endpoints |
| --- | --- |
| Sandboxes | `POST /sandboxes` (launch; optional `envs` object), `GET /sandboxes` (list), `GET /sandboxes/{id}` (status), `DELETE /sandboxes/{id}` (stop), `POST .../pause`, `POST .../resume` |
| Config | `POST /sandboxes/{id}/config` (set idle-timeout or timeout) |
| Gateway | `POST /sandboxes/{id}/gateway/{start,stop,restart,update}`, `GET .../gateway/config`, `PUT .../gateway/config`, `GET .../gateway/doctor` |
| Node | `POST .../node/{connect,reconnect,disconnect}`, `GET .../node/status?gateway_url=X` |
| Devices | `GET /sandboxes/{id}/devices`, `POST .../devices/approve` |
| Services | `POST /sandboxes/{id}/services/setup`, `GET .../services` |
| Pairing | `GET /sandboxes/{id}/pairing?channel=X`, `POST .../pairing/approve`, `POST .../pairing/feishu` |
| Skill | `POST /sandboxes/{id}/skill/install?name=X` |

REST-only extras: `gateway/start` and `gateway/stop` (not in CLI).

Launch environment rules: names match `[A-Za-z_][A-Za-z0-9_]*`, at most 100 variables, and each value is at most 32768 characters. Server-managed proxy variables cannot be overridden. Never echo or log injected values.

## Error Handling

JSON error envelope:

```json
{"status": "error", "error_code": "UPPER_SNAKE_CASE", "message": "...", "remediation": "..."}
```

Only parse `error_code` and `remediation` programmatically (`message` is unstable).

| error_code | exit | Trigger |
| --- | --- | --- |
| `MISSING_API_KEY` | 10 | No API key found |
| `SANDBOX_CREATE_FAILED` | 20 | SDK create() failed |
| `SANDBOX_CONNECT_FAILED` | 21 | Cannot connect to sandbox |
| `SANDBOX_TIMEOUT` | 22 | Creation timed out |
| `GATEWAY_TIMEOUT` | 30 | Health check exhausted |
| `GATEWAY_FAILED` | 31 | Gateway operation failed |
| `GATEWAY_UPDATE_FAILED` | 32 | Update failed |
| `GATEWAY_CONFIG_FAILED` | 33 | Config export/import failed |
| `API_ERROR` | 40 | Generic API error |
| `SERVICE_CONFIG_FAILED` | 50 | Service setup failed |
| `DOCTOR_FAILED` | 60 | Diagnostics failed |
| `PAIRING_FAILED` | 70 | Pairing operation failed |
| `NODE_START_FAILED` | 75 | Node process failed to start |
| `DEVICE_FAILED` | 76 | Device list/approve failed |
| `SANDBOX_PAUSE_FAILED` | 77 | Pause failed |
| `SANDBOX_RESUME_FAILED` | 78 | Resume failed |
| `UPDATE_FAILED` | 80 | CLI self-update failed |
| `CONFIG_FAILED` | 81 | Config set failed |
| `CHANNEL_CONFIG_FAILED` | 82 | Channel configuration failed |

## Important Behaviors

- **Sandbox creation ~60s.** For slow networks: `--timeout 300`.
- **`stop` is irreversible.** Always confirm with the user. Prefer `pause` to preserve state and save costs.
- **`always-on` sandboxes run indefinitely.** Use `--type on-demand` for auto-pause/resume behavior.
- **On-demand auto-pause:** An in-sandbox agent monitors OpenClaw session activity. After consecutive idle checks exceed the threshold, the server automatically pauses the sandbox. Idle timeout is configurable via `config set`.
- **On-demand auto-resume:** HTTP access (webhook, Web UI) automatically resumes a paused sandbox. A cron wakeup scheduler also resumes sandboxes ~120s before their scheduled cron jobs fire.
- **`status` on paused sandboxes is safe.** It returns URLs and credentials from the database without triggering a resume. The response includes a `paused_notice` field.
- **Running on-demand config writes are serialized with resume.** `config set` saves to DB first, skips connect for paused sandboxes, and uses the shared per-sandbox resume operation lock before writing agent config or timeout into a running sandbox.
- **Background schedulers are multi-key and globally bounded.** Idle monitor and cron wakeup must group stored on-demand records by API key before listing live sandboxes, and they share the same monitor concurrency semaphore so their combined sandbox operations stay capped under load.
- **OpenClaw template upgrades require preflight.** Prefer `template/promote-openclaw.sh latest` so the version is resolved once, test/prod build teams are verified before local checks or smoke builds, a disposable smoke template proves the build, the test template is validated by standard E2E plus on-demand stress, stress evidence is summarized automatically, and production is built last. Remote template build scripts use an isolated temporary `DOCKER_CONFIG` so local Docker credentials are not read or rewritten. Test/prod builds fail early when the current build team does not match the target toml `team_id`; set `SKIP_TEMPLATE_TEAM_PREFLIGHT=1` only when this check cannot read the team. Build auth (`ppio-sandbox-cli` login or `PPIO_ACCESS_TOKEN`) and E2E runtime auth (`PPIO_API_KEY`) must belong to the same team when validating a custom template. Use `PROMOTE_STRESS_REPORT_PATH` to choose where promotion stress evidence is written, and use `SANDBOX_BASE_IMAGE` to choose a stable base image tag or digest when the default floating image is unhealthy.
- **Dedicated template bundles are applied at launch.** The image may provide `/etc/openclaw-template/openclaw-overlay.json` and `welcome-message.txt`. The server applies the overlay before injecting the API key, writes public URL metadata to the sandbox runtime directory, adds a non-empty welcome menu and its numeric-selection rule to the workspace `AGENTS.md` Project Context, and displays the welcome message after Gateway readiness.
- **`gateway_token` may be absent** until gateway finishes starting. Re-check with `status` or `doctor --generate-gateway-token`.
- **`doctor --fix --yes`** is the go-to for gateway issues. If it fails, escalate to `gateway update`.

## Recipes

### Full lifecycle

```bash
RESULT=$(ppclaw launch --json)
SID=$(echo "$RESULT" | jq -r '.sandbox_id')
# ... use the sandbox ...
ppclaw stop "$SID" --json
```

### On-demand sandbox (auto-pause when idle)

```bash
RESULT=$(ppclaw launch --type on-demand --idle-timeout 600 --json)
SID=$(echo "$RESULT" | jq -r '.sandbox_id')
# Sandbox auto-pauses after 600s of inactivity, auto-resumes on access.
# Adjust idle timeout at runtime:
ppclaw config set "$SID" idle-timeout 300 --json
```

### Validate on-demand concurrency and OpenClaw template upgrade

```bash
ruff check . && ruff format --check .
pytest tests/test_cli/ tests/test_server/ -v
pytest tests/test_e2e/ tests/test_e2e_hermes/ -v -m e2e --timeout=700 -n 11 --dist loadfile
PPCLAW_E2E_STRESS=1 PPCLAW_STRESS_SANDBOXES=3 PPCLAW_STRESS_WORKERS=3 \
  PPCLAW_STRESS_READ_SWEEPS=2 PPCLAW_STRESS_DUPLICATE_PAUSE_CALLS=3 \
  PPCLAW_STRESS_DUPLICATE_RESUME_CALLS=3 \
  PPCLAW_STRESS_REPORT_PATH=/tmp/ppclaw-ondemand-stress.json \
  pytest tests/test_e2e/test_ondemand_concurrency.py -v -m e2e --timeout=1200 -n 0
python scripts/summarize-stress-report.py /tmp/ppclaw-ondemand-stress.json

cd template
PROMOTE_PRECHECK_ONLY=1 ./promote-openclaw.sh latest
./promote-openclaw.sh latest
./build-docker-preflight.sh latest
./build-smoke.sh latest
./build-smoke-portable.sh latest
```

Set `PROMOTE_PRECHECK_ONLY=1` on `promote-openclaw.sh` after switching credentials to confirm version resolution, template build teams, and required E2E API key presence without building templates, listing sandboxes, or running tests. Set `PPCLAW_STRESS_REPORT_PATH` to save a JSON report with profile settings, phase durations, sandbox IDs/prefixes, and cleanup status for customer-load evidence. The promotion workflow automatically runs `scripts/summarize-stress-report.py <report>` after stress; run it manually after standalone stress runs to produce a Markdown evidence summary and a non-zero exit code when required phases are missing or failed. Add `--json` for machine-readable CI output. The stress flow covers concurrent launch/status/list/config/pause, paused reads that must not wake the sandbox, Web UI HTTP auto-resume, gateway webhook HTTP auto-resume, explicit resume, and stop cleanup.

### Build and launch the exhibition template

```bash
cd template
./build-expo.sh 2026.7.1
EXPO_TEMPLATE_ID=$(sed -n 's/^template_id = "\(.*\)"/\1/p' ppio.expo.toml)
cd ..
ppclaw launch --template-id "$EXPO_TEMPLATE_ID" --json
PPCLAW_E2E_EXPO_TEMPLATE_ID="$EXPO_TEMPLATE_ID" \
  pytest tests/test_e2e/test_expo_template.py -v -m e2e --timeout=700 -n 1
```

For a separate Sandbox test control plane, load the test `.env`, map the runtime API key to build authentication, and use dedicated TOML files so production template IDs are preserved:

```bash
cd template
set -a && source ../.env && set +a
export PPIO_ACCESS_TOKEN="${PPIO_ACCESS_TOKEN:-$PPIO_API_KEY}"
PPCLAW_TEST_TEMPLATE_TOML=ppio.sandbox-test.toml ./build-test.sh 2026.7.1
EXPO_TARGET_TOML=ppio.waic.sandbox-test.toml ./build-expo.sh 2026.7.1
```

The exhibition build copies `skills/expo`, installs each bundled skill's pinned Python requirements, replaces the model catalog with `zai-org/glm-5.2`, and rejects the moving `latest` OpenClaw tag. Sandbox port URLs already use the platform HTTPS mapping, so the publishing skill does not start a reverse proxy. It generates QR codes locally with `qrcode[pil]==8.2`.

The stress test defaults to a 12-sandbox/worker safety limit. For a larger customer-load profile, set both explicit max limits and `PPCLAW_STRESS_ALLOW_LARGE=1`, for example:

```bash
PPCLAW_E2E_STRESS=1 PPCLAW_STRESS_ALLOW_LARGE=1 \
  PPCLAW_STRESS_MAX_SANDBOXES=50 PPCLAW_STRESS_MAX_WORKERS=50 \
  PPCLAW_STRESS_SANDBOXES=20 PPCLAW_STRESS_WORKERS=20 \
  PPCLAW_STRESS_READ_SWEEPS=3 PPCLAW_STRESS_DUPLICATE_PAUSE_CALLS=5 \
  PPCLAW_STRESS_DUPLICATE_RESUME_CALLS=5 \
  PPCLAW_STRESS_REPORT_PATH=/tmp/ppclaw-ondemand-customer-stress.json \
  pytest tests/test_e2e/test_ondemand_concurrency.py -v -m e2e --timeout=1800 -n 0
python scripts/summarize-stress-report.py /tmp/ppclaw-ondemand-customer-stress.json
```

### Pause/resume to save costs

```bash
ppclaw pause "$SID" --json     # no charges while paused
# later...
ppclaw resume "$SID" --json    # restores in ~1s
```

### Fix a broken gateway

```bash
ppclaw doctor "$SID" --fix --yes --json
# If that fails:
ppclaw gateway update "$SID" --json
```

### Configure Feishu channel

```bash
# Event mode (recommended for always-on sandboxes, uses WebSocket long connection)
ppclaw pair feishu "$SID" --app-id cli_xxx --app-secret secret_xxx --json

# Webhook mode (recommended for on-demand sandboxes, enables auto-resume)
# Get verification-token and encrypt-key from Feishu Open Platform:
#   Development > Events & Callbacks > Encryption tab
ppclaw pair feishu "$SID" --app-id cli_xxx --app-secret secret_xxx \
  --mode webhook --verification-token tok_xxx --encrypt-key key_xxx --json

# Approve pairing after the bot replies with a code in Feishu
ppclaw pair list "$SID" feishu --json
ppclaw pair approve "$SID" feishu <CODE> --json
```

### Set up a node connected to a gateway

```bash
RESULT=$(ppclaw launch --mode node --display-name "worker-1" --json)
NODE_ID=$(echo "$RESULT" | jq -r '.sandbox_id')
ppclaw node connect "$NODE_ID" --gateway wss://gateway.example.com --json
# On the gateway side, list pending pairing requests:
ppclaw devices list "$GATEWAY_ID" --json
# Find the pending request ID, then approve it:
REQUEST_ID=$(ppclaw devices list "$GATEWAY_ID" --json | jq -r '.devices[] | select(.status=="pending") | .request_id')
ppclaw devices approve "$GATEWAY_ID" --request-id "$REQUEST_ID" --json
```

### Export and import gateway config

```bash
# Backup current config
ppclaw gateway config export "$SID" -o config-backup.json --json

# Edit and re-import
ppclaw gateway config import "$SID" config-backup.json --json

# Import without restarting (apply later)
ppclaw gateway config import "$SID" config.json --no-restart --json
ppclaw gateway restart "$SID" --json
```

### Install a ClawHub skill

```bash
ppclaw skill install "$SID" sonoscli --json
```
