After an OpenClaw version bump, fix what post-update-awareness noticed. Syncs drifted externalized plugins, restarts the gateway, verifies channel health, an...
---
name: post-update-maintenance
description: After an OpenClaw version bump, fix what post-update-awareness noticed. Syncs drifted externalized plugins, restarts the gateway, verifies channel health, and prints a patch for stale openclaw.json entries. Every mutation is dry-run by default and gated behind --apply or explicit user confirmation. Requires post-update-awareness to be installed (reuses its drift detector). Use after an OpenClaw update when post-update-awareness reports drift or stale config, or when the user asks to fix what the awareness skill flagged.
metadata: {"openclaw":{"requires":{"bins":["openclaw","node","python3","jq"]},"depends":{"skills":["post-update-awareness@>=0.3.1"]}}}
---
# Post-Update Maintenance
This skill **changes things.** Install deliberately.
It pairs with [`post-update-awareness`](https://clawhub.ai/skills/post-update-awareness) — that skill reads release notes and notices broken state; this one fixes it. The split is intentional: awareness is safe to install and run on every heartbeat, maintenance is invoked when you actually want mutations.
## When to use
Run this skill when **any** of the following is true:
1. `post-update-awareness` surfaced drifted plugins or stale config entries and the user wants them resolved.
2. The user says some variant of "fix what awareness flagged" / "sync the drifted plugins" / "clean up the stale config".
3. A safe-update wrapper script is orchestrating a controlled update window and needs the mutation steps.
Do **not** run this skill from a heartbeat. It belongs in an attended session or an explicitly scheduled maintenance job, never as ambient background work.
## Scope
This skill **does**:
- Sync externalized npm-installed plugins that drifted behind the gateway version (`openclaw plugins update <id>`)
- Restart the gateway and wait for `Runtime: running`
- Snapshot channel health before and after, report any newly-broken channels
- Print a JSON patch that removes stale entries from `openclaw.json` (and apply it only on explicit confirmation)
- Back up `openclaw.json` before any mutation; if post-restart gateway is unhealthy, restore the backup
This skill **does not**:
- Read or distill CHANGELOG content (that's `post-update-awareness`)
- Install missing native dependencies (`sharp`, `ffmpeg`, `node-pty`) — those need package-manager context this skill shouldn't have
- Roll back OpenClaw to a previous version (that's your update-guard, e.g. `openclaw-update-guard.sh`)
- Modify any plugin config keys beyond removing stale top-level `plugins.entries.<id>` entries flagged by the gateway as "disabled but configured"
- Run unattended
## Dependency: post-update-awareness
This skill **requires** `post-update-awareness` >= 0.3.1 to be installed. It reuses two scripts from that skill:
- `scripts/resolve.sh` — locates the awareness install across known layouts
- `scripts/check-plugin-drift.sh` — the authoritative drift detector
If awareness is not installed, the skill exits with a clear error pointing the user at `clawhub install post-update-awareness`. The two skills are versioned independently — pin a compatible awareness version in `_meta.json` if you find a regression.
Why the hard dep instead of vendoring: vendoring the drift detector in two skills would re-create exactly the drift problem this whole pair exists to solve. One source of truth.
## Workflow
### 1) Pre-flight
```bash
{baseDir}/scripts/preflight.sh <profile>
```
The script:
- Confirms `post-update-awareness` is installed (via its `resolve.sh`)
- Confirms gateway is healthy *before* the maintenance run (refuses to start if not — that's the update-guard's job, not this skill's)
- Confirms `openclaw.json` is readable and writable
- Computes a SHA256 of the current config and stashes a timestamped backup at `${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/openclaw.json.<epoch>`
- Snapshots `channels status` JSON to `${OPENCLAW_PROFILE_DIR}/post-update-maintenance/snapshots/channels-before.<epoch>.json`
If any check fails, the script prints `BLOCKED <reason>` on stdout and exits 2. The skill must stop here and surface the reason to the user. Do not attempt to repair pre-flight failures — that's outside scope.
### 2) Detect drift (read-only, via awareness)
```bash
{baseDir}/scripts/detect-drift.sh <profile>
```
Internally this calls `scripts/check-plugin-drift.sh` from `post-update-awareness` via the resolver. Output is the same `DRIFT <plugin-id> <version> (gateway <gw>)` lines, one per drifted plugin.
If output is empty → skip to step 4 (config cleanup may still have work to do).
### 3) Sync drifted plugins (mutation — gated)
```bash
{baseDir}/scripts/sync-drifted-plugins.sh <profile> [--apply] [--yes]
```
**Default mode is dry-run.** Without `--apply`, the script prints the exact `openclaw plugins update <id>` commands it would run, one per line, and exits 0. Nothing changes.
With `--apply`, the script:
1. For each drifted plugin: runs `openclaw --profile <profile> plugins update <plugin-id>` (sequential, not parallel — plugin updates can rewrite `openclaw.json`).
2. After all syncs: runs `openclaw --profile <profile> gateway restart`.
3. Polls `gateway status` for up to 60 seconds waiting for `Runtime: running`.
4. If healthy → proceed.
5. If unhealthy → restore the config backup from step 1, restart again, exit 3 with `RESTORE <reason>`.
`--yes` skips the per-plugin confirmation prompt when running attended. Without `--yes`, the skill should prompt the user before each plugin (good practice when one of the drifted plugins is a sensitive transport like `@openclaw/whatsapp`).
Plugin updates often rewrite `openclaw.json` and log lines like:
```
Config overwrite: <path> (sha256 <old> -> <new>, backup=<path>.bak)
```
The script captures these and includes them in the run report. The user-facing summary should mention every config rewrite and where the `.bak` lives, even on success — this is information they need.
### 4) Detect stale config entries (read-only)
```bash
{baseDir}/scripts/detect-stale-config.sh <profile>
```
Parses `openclaw --profile <profile> gateway status` (and `plugins list`) for lines like:
```
plugins.entries.<id>: plugin disabled (...) but config is present
```
Output: one stale entry per line, in the form:
```
STALE plugins.entries.<id>
```
These entries are safe to remove but the gateway leaves them in place because removing config the user wrote is risky without explicit consent.
### 5) Clean stale config entries (mutation — gated)
```bash
{baseDir}/scripts/clean-stale-config.sh <profile> [--apply] [--entries <id1,id2,...>]
```
**Default mode is dry-run.** Without `--apply`:
- Prints the JSON patch that would be applied (in [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) style: one `{ "op": "remove", "path": "/plugins/entries/<id>" }` per stale entry)
- Lists the file paths that would change
- Exits 0
With `--apply`:
1. Re-reads `openclaw.json` (defensive — config may have changed since pre-flight if step 3 ran)
2. Computes a fresh SHA256 backup at `${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/openclaw.json.<epoch>`
3. Applies the patch using `jq` (or a Python fallback) — atomic write via `<file>.tmp` + `mv`
4. Asks the gateway to reload config: `openclaw --profile <profile> gateway restart`
5. Polls for healthy
6. If unhealthy → restore the backup, restart, exit 3 with `RESTORE <reason>`
`--entries` lets the user opt into a subset rather than all detected stale entries. Useful when one of the entries is something they intend to re-enable later.
### 6) Channel health verification (read-only)
```bash
{baseDir}/scripts/verify-channels.sh <profile>
```
Snapshots `channels status` JSON, diffs against the pre-flight snapshot. Output:
- `OK_HEALTHY <channel-id>` — was healthy, still healthy
- `RECOVERED <channel-id>` — was unhealthy before, now healthy (good news to surface)
- `BROKE <channel-id> <reason>` — was healthy before, now unhealthy (bad news; surface loudly)
- `STILL_BROKEN <channel-id> <reason>` — was unhealthy before, still unhealthy (mention, but don't blame this skill)
The pre-flight + post-mutation pairing matters: a channel that was already disconnected before the maintenance run isn't a regression caused by this skill, and the report should distinguish.
### 7) Surface the result
Send **one** brief message via the active channel.
```
Maintenance complete on profile <profile>.
✅ Synced: @openclaw/whatsapp 2026.5.3 → 2026.5.5
✅ Cleaned 2 stale config entries (plugins.entries.device-pair, plugins.entries.cognee-openclaw)
✅ Gateway restarted, runtime healthy
Channels:
- WhatsApp default: healthy (unchanged)
- Telegram default: healthy (unchanged)
Backups: ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/
Logs: ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/runs/<epoch>.log
```
If anything restored:
```
⚠️ Maintenance rolled back on profile <profile>.
Attempted: sync @openclaw/whatsapp 2026.5.3 → 2026.5.5
Result: gateway unhealthy after restart — restored openclaw.json from backup.
Restored config: ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/openclaw.json.<epoch>
Restart log: ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/runs/<epoch>.log
Recommend investigating before retrying. The gateway is now running the previous config.
```
Hard cap: ~18 lines, same as awareness. Drop empty sections.
### 8) Persist run state
Write a JSON run record to `${OPENCLAW_PROFILE_DIR}/post-update-maintenance/runs/<epoch>.json` containing:
- The profile
- The pre-flight config SHA256 and backup path
- Each drifted plugin attempted and its outcome
- Each stale entry attempted and its outcome
- Pre/post channel health diff
- The final gateway runtime status
- Restore status if any
This run history lets the user (or a future agent) audit what happened without re-running anything.
## Path conventions
All state lives under `${OPENCLAW_PROFILE_DIR}/post-update-maintenance/`, resolved in the same order as `post-update-awareness`:
1. `$OPENCLAW_PROFILE_DIR/post-update-maintenance/`
2. `$HOME/.openclaw-<profile>/post-update-maintenance/`
3. `$HOME/.openclaw/post-update-maintenance/`
Subdirectories:
- `backups/` — `openclaw.json.<epoch>` snapshots taken before every mutation
- `snapshots/` — `channels-before.<epoch>.json` and `channels-after.<epoch>.json`
- `runs/` — `<epoch>.json` run record + `<epoch>.log` raw command output
Old backups and run logs older than 30 days are not auto-pruned. The skill is not allowed to delete anything in `${OPENCLAW_PROFILE_DIR}` — that's the user's data.
## Safety contract
This skill respects the following invariants:
1. **Default is dry-run.** Every mutating script requires `--apply` to actually change anything.
2. **Every mutation has a backup.** Config snapshots taken immediately before each mutation, never reused across steps.
3. **Every mutation is reversible.** Backup restore is the first response to post-mutation unhealth.
4. **No silent failure.** If anything fails, the skill exits non-zero with a clear `RESTORE <reason>` or `BLOCKED <reason>` line.
5. **No version rollback.** This skill never runs `npm install -g openclaw@<version>`. Version-level recovery is the update-guard's job.
6. **No native-dep install.** Optional native modules (`sharp`, `ffmpeg`, `node-pty`) are out of scope. The skill assumes the host package manager has them or doesn't.
7. **Read-only awareness fallback.** When the drift detector reports no drift and the stale-config detector reports nothing to remove, the skill exits with `NOTHING_TO_DO` and doesn't restart the gateway.
## Voice
Operational. Terse. Factual. Match the awareness skill's tone — this is not marketing.
- ✅ "Synced @openclaw/whatsapp 2026.5.3 → 2026.5.5. Gateway restarted, healthy. WhatsApp default reconnected after 14s."
- ❌ "🎉 Successfully completed maintenance with zero issues!"
## Failure modes
| Situation | Behavior |
|---|---|
| `post-update-awareness` not installed | `BLOCKED missing-dep: post-update-awareness`; tell user to `clawhub install post-update-awareness` first |
| Pre-flight gateway unhealthy | `BLOCKED gateway-unhealthy`; refuse to start — update-guard's territory |
| Plugin update fails | Skip that plugin, continue with the rest, report in run record. Don't restart unless at least one succeeded. |
| Gateway unhealthy after sync | Restore backup, restart, exit 3 |
| Gateway unhealthy after stale-config clean | Restore backup, restart, exit 3 |
| `--apply` passed but no drift and no stale entries | `NOTHING_TO_DO`; exit 0 cleanly |
| `jq` not on PATH | Fall back to a Python json-patch implementation; if both missing, `BLOCKED missing-tool: jq or python3` |
## Why this exists
`post-update-awareness` surfaces problems. Users were then expected to remember the exact commands to fix each one — and to remember to back up `openclaw.json` first, and to restart the gateway, and to check that channels didn't break. In practice they ran the wrong subset, or forgot the backup, or didn't notice when WhatsApp came back as `not-linked`.
This skill is the small, deliberate, gated mutation layer that closes that loop. It's separate from awareness so the trust posture is clear: awareness is safe to install everywhere, maintenance is invoked when you want changes.
don't have the plugin yet? install it then click "run inline in claude" again.
extracted intent, inputs, procedure steps with explicit input/output per step, decision points as if-else branches, output contract with file locations and exit codes, and outcome signal with observable success criteria and safety invariants.
this skill changes things. install deliberately.
it pairs with post-update-awareness , that skill reads release notes and notices broken state, this one fixes it. the split is intentional: awareness is safe to install and run on every heartbeat, maintenance is invoked when you actually want mutations.
run this skill when post-update-awareness surfaces drifted plugins or stale config entries and the user wants them resolved. use it in attended sessions or explicitly scheduled maintenance windows, never as ambient background work. every mutation is dry-run by default and gated behind --apply or explicit user confirmation. the skill syncs npm-installed plugins that drifted behind the gateway version, restarts the gateway, snapshots channel health before and after to report regressions, and prints a json patch to remove stale openclaw.json entries flagged by the gateway as disabled-but-configured.
required binaries on PATH:
openclaw , the cli toolnode , for plugin operationspython3 , json fallback if jq unavailablejq , for config patching (optional, python fallback available)required skill dependency:
post-update-awareness >= 0.3.1 (reused: scripts/resolve.sh, scripts/check-plugin-drift.sh)BLOCKED missing-dep: post-update-awareness and points user to clawhub install post-update-awarenessenvironment and file state:
${OPENCLAW_PROFILE_DIR} , resolved in order: $OPENCLAW_PROFILE_DIR/post-update-maintenance/, $HOME/.openclaw-<profile>/post-update-maintenance/, $HOME/.openclaw/post-update-maintenance/openclaw.json , must be readable and writable, located where openclaw --profile <profile> expects itpost-update-awareness must be installed and locatable via its resolve.sh scriptoptional user inputs:
--apply flag to actually mutate (default is dry-run)--yes flag to skip per-plugin confirmation prompts--entries <id1,id2,...> to subset which stale config entries to cleaninput: <profile> (openclaw profile name)
run {baseDir}/scripts/preflight.sh <profile>
resolve.shopenclaw.json is readable and writable${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/openclaw.json.<epoch>channels status json to ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/snapshots/channels-before.<epoch>.jsonoutput: exit 0 on success, exit 2 with BLOCKED <reason> on failure. skill must stop and surface reason to user if exit is non-zero. do not attempt to repair pre-flight failures.
input: <profile>
run {baseDir}/scripts/detect-drift.sh <profile>
internally calls scripts/check-plugin-drift.sh from post-update-awareness via the resolver.
output: one line per drifted plugin in form DRIFT <plugin-id> <version> (gateway <gw>). if output is empty, skip to step 4 (config cleanup may still have work). if any plugins are drifted, proceed to step 3.
input: <profile>, optional --apply, optional --yes
run {baseDir}/scripts/sync-drifted-plugins.sh <profile> [--apply] [--yes]
default mode is dry-run: without --apply, script prints exact openclaw plugins update <id> commands it would run, one per line, and exits 0. nothing changes.
with --apply:
openclaw --profile <profile> plugins update <plugin-id> (sequential, not parallel , plugin updates can rewrite openclaw.json)openclaw --profile <profile> gateway restartgateway status for up to 60 seconds waiting for Runtime: runningRESTORE <reason>with --yes: skips per-plugin confirmation prompt. without it, skill should prompt user before each plugin update (good practice for sensitive transports like @openclaw/whatsapp).
plugin updates often rewrite openclaw.json and log lines like Config overwrite: <path> (sha256 <old> -> <new>, backup=<path>.bak). capture these and include in run report , user needs to know about every config rewrite and where the .bak lives.
output: on success, list of synced plugins with version changes. on failure, exit 3 with RESTORE <reason> and detailed logs. run record written at step 8.
input: <profile>
run {baseDir}/scripts/detect-stale-config.sh <profile>
parse openclaw --profile <profile> gateway status and plugins list for lines like plugins.entries.<id>: plugin disabled (...) but config is present.
output: one stale entry per line in form STALE plugins.entries.<id>. if output is empty, both drift and stale detection found nothing , skip to step 7 and exit with NOTHING_TO_DO.
input: <profile>, optional --apply, optional --entries <id1,id2,...>
run {baseDir}/scripts/clean-stale-config.sh <profile> [--apply] [--entries <id1,id2,...>]
default mode is dry-run: without --apply:
{ "op": "remove", "path": "/plugins/entries/<id>" } per stale entry)with --apply:
openclaw.json (defensive , config may have changed since pre-flight if step 3 ran)${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/openclaw.json.<epoch>jq (or python fallback) , atomic write via <file>.tmp + mvopenclaw --profile <profile> gateway restartRESTORE <reason>--entries <id1,id2,...> lets user opt into subset rather than all detected stale entries. useful when one entry is something they intend to re-enable later.
output: on success, list of removed config entries. on failure, exit 3 with RESTORE <reason>.
input: <profile>, pre-flight snapshot from step 1
run {baseDir}/scripts/verify-channels.sh <profile>
snapshot channels status json, diff against pre-flight snapshot.
output: one line per channel in form:
OK_HEALTHY <channel-id> , was healthy, still healthyRECOVERED <channel-id> , was unhealthy before, now healthyBROKE <channel-id> <reason> , was healthy before, now unhealthySTILL_BROKEN <channel-id> <reason> , was unhealthy before, still unhealthythe pre-flight + post-mutation pairing matters: a channel that was already disconnected before maintenance isn't a regression caused by this skill, and the report distinguishes.
input: run state from steps 1-6
send one brief message via active channel. if nothing to do, say so and exit cleanly:
No drifted plugins or stale config entries on profile <profile>.
on success:
Maintenance complete on profile <profile>.
✅ Synced: @openclaw/whatsapp 2026.5.3 → 2026.5.5
✅ Cleaned 2 stale config entries (plugins.entries.device-pair, plugins.entries.cognee-openclaw)
✅ Gateway restarted, runtime healthy
Channels:
- WhatsApp default: healthy (unchanged)
- Telegram default: healthy (recovered after 14s)
Backups: ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/
Logs: ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/runs/<epoch>.log
if anything restored:
⚠️ Maintenance rolled back on profile <profile>.
Attempted: sync @openclaw/whatsapp 2026.5.3 → 2026.5.5
Result: gateway unhealthy after restart , restored openclaw.json from backup.
Restored config: ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/openclaw.json.<epoch>
Restart log: ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/runs/<epoch>.log
Recommend investigating before retrying. Gateway is running the previous config.
hard cap: 18 lines, drop empty sections.
input: all state from steps 1-7
write json run record to ${OPENCLAW_PROFILE_DIR}/post-update-maintenance/runs/<epoch>.json:
this run history lets user audit what happened without re-running.
output: json file written atomically.
if post-update-awareness is not installed: exit with BLOCKED missing-dep: post-update-awareness. point user to clawhub install post-update-awareness first.
if pre-flight gateway is unhealthy: exit with BLOCKED gateway-unhealthy. refuse to start. this is the update-guard's territory, not this skill's. do not attempt to repair.
if drift detected but no --apply flag: print dry-run commands and exit 0. nothing changes.
if stale config detected but no --apply flag: print json patch and exit 0. nothing changes.
if plugin update fails for one plugin: skip that plugin, continue with rest, report in run record. do not restart gateway unless at least one plugin succeeded.
if gateway unhealthy after sync or stale-config clean: restore config backup from its corresponding step, restart gateway, exit 3 with RESTORE <reason>. do not attempt to proceed.
if --apply passed but no drift and no stale entries found: exit 0 cleanly with NOTHING_TO_DO. do not restart gateway.
if jq not on PATH: fall back to python json-patch implementation. if both jq and python3 missing, exit with BLOCKED missing-tool: jq or python3.
if user passes --entries <id1,id2,...> but some entries are not in the detected stale list: warn but proceed only with entries that are actually stale.
success formats:
${OPENCLAW_PROFILE_DIR}/post-update-maintenance/runs/<epoch>.json containing profile, config sha256, plugin updates, stale entries removed, pre and post channel health, final gateway status, restore status if any${OPENCLAW_PROFILE_DIR}/post-update-maintenance/snapshots/channels-before.<epoch>.json and channels-after.<epoch>.json${OPENCLAW_PROFILE_DIR}/post-update-maintenance/backups/openclaw.json.<epoch>, one per mutation stepfile locations:
${OPENCLAW_PROFILE_DIR}/post-update-maintenance/, resolved in order: explicit env var, then $HOME/.openclaw-<profile>/, then $HOME/.openclaw/backups/ (config snapshots), snapshots/ (channel health), runs/ (run records + logs)exit codes:
no auto-pruning: old backups and run logs older than 30 days are not pruned. the skill is not allowed to delete anything in ${OPENCLAW_PROFILE_DIR} , that's user data.
the user knows the skill worked when:
--apply and the mutation ran cleanly${OPENCLAW_PROFILE_DIR}/post-update-maintenance/runs/<epoch>.json to see exactly what happened without re-runningthe skill also respects these invariants:
--applyRESTORE <reason> or BLOCKED <reason> linenpm install -g openclaw@<version> , that's the update-guard's jobNOTHING_TO_DO and doesn't restart the gateway