---
name: pphermes
description: >
  Manage Hermes Agent sandboxes on PPIO cloud via CLI or REST API.
  Use this skill whenever the user wants to launch, stop, pause, resume, diagnose, or configure a Hermes Agent sandbox, manage the Hermes gateway, export or import Hermes config, pair Feishu / Lark channels,
  or when they mention PPHermes, Hermes Agent.
argument-hint: "<command> [sandbox_id]"
---

# PPHermes

PPHermes launches [Hermes Agent](https://github.com/NousResearch/hermes-agent) instances on [PPIO Agent Sandbox](https://ppio.com/docs/sandbox/overview.md). It creates a cloud sandbox, configures Hermes with LLM providers and gateway, then exposes Web UI and service URLs.

## Onboarding

Walk first-time users through these steps:

1. **Install:**
   - macOS / Linux: `curl -fsSL https://pphermes.ppio.com/install.sh | bash`
   - Windows (PowerShell): `irm https://pphermes.ppio.com/install.ps1 | iex`
   - Or: `pip install pphermes`
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:** `pphermes launch --json` (~60s, returns `sandbox_id`, Hermes Web UI URL, credentials)
5. **Verify:** `pphermes list --json`
6. **Cleanup:** `pphermes 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.

## 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 |
| --- | --- |
| `pphermes launch [--timeout 180] [--type always-on\|on-demand] [--idle-timeout 300]` | Create sandbox (~60s). Returns `sandbox_id`, `hermes_web_url`, `terminal_url`, `filemanager_url`, credentials. |
| `pphermes list` | List active Hermes sandboxes: `{sandbox_id, state, sandbox_type, cpu, memory_mb}`. |
| `pphermes status <id>` | Sandbox state, URLs, credentials. Safe on paused sandboxes (does not trigger resume). |
| `pphermes pause <id>` | Pause sandbox. State preserved, no charges. |
| `pphermes resume <id>` | Resume paused sandbox (~1s). |
| `pphermes stop <id>` | **Irreversible.** Destroys sandbox. Prefer `pause` to save costs. |

### Gateway

| Command | Description |
| --- | --- |
| `pphermes gateway update <id>` | Update Hermes Agent + restart gateway. |
| `pphermes gateway restart <id>` | Restart gateway without upgrading. |
| `pphermes gateway config-export <id> [-o FILE]` | Export Hermes config.yaml. |
| `pphermes gateway config-import <id> <FILE> [--no-restart]` | Import config.yaml (restarts gateway by default). |

### Diagnostics

| Command | Description |
| --- | --- |
| `pphermes doctor <id>` | Health check: is the Hermes gateway process running? |

### Feishu / Lark Channel Configuration

| Command | Description |
| --- | --- |
| `pphermes pair feishu <id> --app-id <ID> --app-secret <SECRET> [--mode websocket\|webhook]` | Configure Feishu channel. Default mode is `websocket`. For `webhook` mode, `--verification-token` is required. Additional options: `--domain feishu\|lark`, `--allowed-users`, `--home-channel`. |

### Migration from OpenClaw

| Command | Description |
| --- | --- |
| `pphermes migrate <hermes_id> --from <openclaw_id> [--preset full\|user-data] [--overwrite] [--exclude-tts] [--dry-run] [--print-workspace-guide] [--include-workspace] [--workspace-path <PATH> ...] [--workspace-target <DIR>] [--workspace-max-size <SIZE>] [--force-size] [--workspace-exclude <CSV>] [-y]` | **Sandbox-to-sandbox mode.** Tar `~/.openclaw` on the source sandbox, upload directly to the target's File Manager (bytes never cross this API server), then invoke `hermes claw migrate` on the target to import SOUL.md, memories, skills, command allowlist, messaging settings, secrets, TTS assets and AGENTS.md. Default preset `full` migrates everything including secrets; `user-data` skips secrets. `--overwrite` replaces existing Hermes files (otherwise conflicts are skipped and surfaced). `--exclude-tts` drops bulky voice cache before upload. `--dry-run` previews with no writes. `--print-workspace-guide` prints File Manager URLs for a manual workspace copy. `--include-workspace` (opt-in) also packs `~/workspace/`, `~/projects/` (override with `--workspace-path`, repeatable) and extracts them under `~/workspace/from-openclaw/` on the target (override with `--workspace-target`); size-gated at 2 GiB (override with `--workspace-max-size 2GB\|500MiB` or bypass with `--force-size`); excludes default to `node_modules,.venv,__pycache__,.cache,target,dist,build,.git` (override with `--workspace-exclude "pat1,pat2"`). |
| `pphermes migrate <hermes_id> --from-local [--openclaw-path <PATH>] [--no-openclaw] [--workspace-path <PATH> ...] [other migration flags]` | **Local-machine mode.** Pack `~/.openclaw` (or `--openclaw-path`) and any `--workspace-path` directories *on your laptop*, upload them to the target sandbox's File Manager via HTTP Basic Auth multipart POST, then trigger the same import on the target. Use `--no-openclaw` to skip the .openclaw bundle and only transfer workspace directories. Mutually exclusive with `--from`. All `--preset / --overwrite / --dry-run / --exclude-tts / --workspace-*` flags behave identically to sandbox-to-sandbox mode. `--print-workspace-guide` is not supported here. |

## REST API

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

| Pattern | Endpoints |
| --- | --- |
| Sandboxes | `POST /sandboxes` (launch), `GET /sandboxes` (list), `GET /sandboxes/{id}` (status), `DELETE /sandboxes/{id}` (stop), `POST .../pause`, `POST .../resume` |
| Gateway | `POST /sandboxes/{id}/gateway/{start,stop,restart,update}`, `GET .../gateway/config`, `PUT .../gateway/config`, `GET .../gateway/doctor` |
| Pairing | `POST /sandboxes/{id}/pairing/feishu` |
| Migration | `POST /sandboxes/{id}/migrate-from-openclaw` (sandbox-to-sandbox; body: `{source_sandbox_id, preset, overwrite, dry_run, exclude_tts, include_workspace, workspace_paths, workspace_target, workspace_max_bytes, workspace_force_size, workspace_exclude}`; query: `workspace_guide=true` to include File Manager guide), `POST /sandboxes/{id}/finalize-uploaded-migration` (local-machine mode; body: `{openclaw_tarball?, workspace_tarballs?: [{tarball, target_path}], preset, overwrite, dry_run, run_health_check}`; at least one of `openclaw_tarball`/`workspace_tarballs` required; tarball names must be plain filenames — the CLI uploads them to `/home/user/<name>` on the target's File Manager first) |

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

## 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 |
| `API_ERROR` | 40 | Generic API error |
| `SERVICE_CONFIG_FAILED` | 50 | Service setup failed |
| `DOCTOR_FAILED` | 60 | Diagnostics 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 |
| `MIGRATION_FAILED` | 82 | OpenClaw → Hermes migration failed (missing source `~/.openclaw`, upload or `hermes claw migrate` non-zero exit, missing target File Manager credentials, SDK error) |
| `INVALID_USAGE` | 2 | Bad CLI flag combo for `migrate` (e.g. `--from` + `--from-local`, same source/target sandbox ID, `--openclaw-path` without `--from-local`) |

## 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.
- **Default model:** `minimax/minimax-m2.5-highspeed` via PPIO OpenAI-compatible endpoint.
- **Config format:** Hermes uses YAML (`config.yaml`), not JSON like OpenClaw.
- **Webhook mode** does not support Feishu Encrypt Key (Hermes Gateway limitation). Leave Encrypt Key empty in Feishu Open Platform.
- **Post-migrate health check (non-dry-run only):** every successful `pphermes migrate` finishes with an automatic sanitizer + chat probe that (a) strips the bogus provider prefix off `model.default` (upstream bug that otherwise triggers `404 MODEL_NOT_FOUND` on the next chat), (b) dedupes `custom_providers` entries by `(name, base_url)`, and (c) fires a tiny `hermes chat -Q -q "..."` probe to confirm the model actually responds. Findings are auto-applied, the gateway is restarted, and the outcome is attached under `response.health_check` / a green-or-yellow panel in the CLI. `--dry-run` skips this step. See `docs/design/migration.md` §4a.

## Recipes

### Full lifecycle

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

### Pause/resume to save costs

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

### Configure Feishu channel (WebSocket mode, recommended)

```bash
pphermes pair feishu "$SID" --app-id cli_xxx --app-secret secret_xxx --json
```

### Configure Feishu channel (Webhook mode)

```bash
pphermes pair feishu "$SID" --app-id cli_xxx --app-secret secret_xxx \
  --mode webhook --verification-token tok_xxx --json
# Set callback URL in Feishu Open Platform:
# https://8765-<sandbox_id>.sandbox.ppio.cn/feishu/webhook
```

### Export and import Hermes config

```bash
pphermes gateway config-export "$SID" -o config-backup.yaml --json
# Edit the YAML config...
pphermes gateway config-import "$SID" config-backup.yaml --json
# Or import without restarting:
pphermes gateway config-import "$SID" config.yaml --no-restart --json
pphermes gateway restart "$SID" --json
```

### Migrate from an existing OpenClaw sandbox

```bash
# Launch a fresh Hermes sandbox
HID=$(pphermes launch --json | jq -r '.sandbox_id')

# Preview the migration (no writes on the target):
pphermes migrate "$HID" --from sbx_openclaw_xxx --dry-run --json

# Apply: tar ~/.openclaw on the source, upload directly to the target's File
# Manager, then invoke `hermes claw migrate` on the target:
pphermes migrate "$HID" --from sbx_openclaw_xxx -y --json

# Skip secrets (.env on the target is kept as-is):
pphermes migrate "$HID" --from sbx_openclaw_xxx --preset user-data -y

# Replace existing Hermes files on conflict (otherwise conflicts are skipped):
pphermes migrate "$HID" --from sbx_openclaw_xxx --overwrite -y

# Drop bulky TTS audio to save bandwidth:
pphermes migrate "$HID" --from sbx_openclaw_xxx --exclude-tts -y

# Also print a guide for manually copying workspace files via File Manager:
pphermes migrate "$HID" --from sbx_openclaw_xxx --print-workspace-guide -y

# Also transfer workspace files (e.g. ~/workspace, ~/projects) directly:
pphermes migrate "$HID" --from sbx_openclaw_xxx --include-workspace -y

# Larger workspace + custom scope (repeatable --workspace-path):
pphermes migrate "$HID" --from sbx_openclaw_xxx --include-workspace \
  --workspace-path "~/code/" --workspace-path "~/notes/" \
  --workspace-target "~/workspace/from-claw/" \
  --workspace-max-size 5GiB -y

# Bypass the 2 GiB safety gate and override excludes:
pphermes migrate "$HID" --from sbx_openclaw_xxx --include-workspace \
  --force-size --workspace-exclude "node_modules,.git,dist" -y
```

The tarball travels directly source → PPIO edge → target File Manager — bytes
never cross this API server. Workspace files (`~/workspace/`, local repos) are
**not** transferred by default — pass `--include-workspace` to opt in. Files
land under `~/workspace/from-openclaw/` on the target so Hermes's own
`~/workspace` is preserved. The transfer is size-gated (default 2 GiB); use
`--workspace-max-size` to raise the cap or `--force-size` to bypass. Use
`--print-workspace-guide` for a manual, File-Manager-driven walkthrough
instead. See `docs/design/migration.md`.

### Migrate from a local machine (no source sandbox needed)

```bash
# Launch target first
HID=$(pphermes launch --json | jq -r '.sandbox_id')

# Pack local ~/.openclaw + upload + run import (secrets included):
pphermes migrate "$HID" --from-local -y --json

# Also carry a couple of workspace directories along:
pphermes migrate "$HID" --from-local \
  --workspace-path ~/code/my-agent \
  --workspace-path ~/notes \
  --workspace-target "~/workspace/from-openclaw/" -y --json

# Workspace-only transfer (skip ~/.openclaw entirely):
pphermes migrate "$HID" --from-local --no-openclaw \
  --workspace-path ~/code/my-agent -y --json

# Alternate local .openclaw location + size override:
pphermes migrate "$HID" --from-local \
  --openclaw-path ~/backups/openclaw-2026-04 \
  --workspace-path ~/code --workspace-max-size 5GiB -y --json
```

Local-machine mode is for users who never had an OpenClaw sandbox but want
to seed a fresh Hermes sandbox from their laptop (or to push arbitrary
workspace folders from outside the sandbox network). The CLI reads
`~/.openclaw` and `--workspace-path` directories, tars + gzips them
locally, and streams them into the target sandbox's File Manager
(`:7682` + HTTP Basic Auth). The server-side import reuses the same
`finalize_openclaw_import` / `finalize_workspace_extract` helpers as the
sandbox-to-sandbox path, so the health check, conflict reporting and
preset semantics are identical.
