Install
openclaw skills install safe-update-mergeClawHub Security found sensitive or high-impact capabilities. Review the scan results before using.
Safely merge upstream OpenClaw updates without destroying plugin/skill injections, custom UI tabs, or workspace features. Two-phase: Phase 1 (automated) merges, builds, and restarts the gateway from a safe-merge branch — without touching local-desktop-main. Phase 2 (user-confirmed) promotes to local-desktop-main and pushes to the fork remote only after the user verifies the gateway is healthy.
openclaw skills install safe-update-mergeMerges upstream OpenClaw changes into your fork while preserving all custom code: plugin registrations, custom UI tabs, workspace skills, controllers, and state extensions.
This skill performs high-impact, partially irreversible operations. Understand what it does before executing in a production environment:
| Operation | Phase | Impact |
|---|---|---|
git merge upstream/main | Phase 1 | Creates safe-merge-YYYY-MM-DD branch and merges — no changes to local-desktop-main |
npm run build / pnpm run build | Phase 1 | Downloads packages if lockfile changed (network); builds in place |
systemctl --user restart openclaw-gateway | Phase 1 | Restarts the live gateway — brief downtime; gateway now runs from safe-merge build |
Conflict resolution via claude CLI | Phase 1 (optional) | Passes redacted file content to the Claude API; Edit+Read tools only — Bash explicitly excluded |
git push --force to TARGET_REMOTE/TARGET_BRANCH | Phase 2 only (--promote) | Overwrites remote branch — only runs after user confirms gateway is healthy |
git branch -D safe-merge-* | Phase 2 only (--promote) | Deletes temp branch after successful promotion |
Before running, verify:
git remote -v shows upstream pointing to github.com/openclaw/openclaw (or set UPSTREAM_REMOTE)git remote -v shows myfork pointing to your fork (or set TARGET_REMOTE)local-desktop-main exists locally (or set TARGET_BRANCH)claude CLI is installed and authenticated (claude --version)npm or pnpm is available for buildsWhen merge conflicts occur, the script:
scripts/redact-secrets.sh on each conflicted file — replaces secrets with [REDACTED_N] placeholders, writes the redaction map to a per-file temp file in a mode-700 directoryclaude CLI with --allowedTools Edit,Read (no Bash) to resolve conflict markers in the redacted files — the model cannot execute shell commandsgit add -A && git commit --no-edit itselfIf you prefer manual resolution, skip claude entirely: fix conflicts yourself and run --resume.
| Flag | Description |
|---|---|
--dry-run | Fetch remotes, show divergence and estimated conflict count, then exit — no changes made |
--no-auto-resolve | Stop on conflicts instead of invoking claude — leaves the safe-merge branch for manual resolution, then use --resume |
--resume <branch> | Skip the merge; run build → restart on an existing safe-merge branch (use after manual conflict resolution) |
--promote <branch> | Phase 2 — run after user confirms the gateway is healthy. Force-pushes the safe-merge branch to TARGET_REMOTE/TARGET_BRANCH, switches local branch, deletes temp branch. This is the only step that modifies local-desktop-main. |
| Variable | Default | Description |
|---|---|---|
REPO_DIR | (required) | Path to your OpenClaw repository |
UPSTREAM_REMOTE | upstream | Remote name for openclaw/openclaw |
UPSTREAM_BRANCH | main | Branch to pull from upstream |
TARGET_REMOTE | myfork | Remote to force-push result to |
TARGET_BRANCH | local-desktop-main | Branch to promote on success |
LOCAL_BRANCH | current branch | The branch being merged from (used by preflight.sh for divergence stats) |
PACKAGE_MGR | auto-detect | npm or pnpm (auto-detected from lockfile) |
This skill uses the claude CLI (the Anthropic Claude Code CLI, not the OpenClaw agent model) for conflict resolution. The CLI must be installed separately and authenticated with an Anthropic API key in your environment. It is invoked with --allowedTools Edit,Read — the Bash tool is explicitly excluded, so the model can only read and edit files, not execute shell commands.
The model sees only the redacted content of conflicted files (see Secret Redaction below) — not the entire repository.
To control which Claude model is used, configure it in your ~/.claude/config.json or set the model via claude CLI flags.
Before any file content is sent to the claude CLI for conflict resolution, it passes through scripts/redact-secrets.sh which detects and replaces:
sk-, GitHub ghp_, Slack xoxb-, AWS AKIA, etc.)password, secret, token, apiKey fieldsDetected secrets are replaced with [REDACTED_N] placeholders.
How the map is stored: redact-secrets.sh requires fd 3 to be open and writes the redaction map to whatever file fd 3 points to. In safe-merge-update.sh, fd 3 is wired to a per-file temp file inside a mode-700 temp directory (mktemp -d). So the map is written to disk — in a private, process-owned temp directory — and deleted immediately after secret restoration. It is never written to stdout, stderr, or any shared/world-readable path.
If your security policy requires no disk writes, mount the temp dir on a tmpfs:
REDACT_MAP_DIR=$(mktemp -d)
mount -t tmpfs -o size=1m,mode=700 tmpfs "$REDACT_MAP_DIR"
# ... run safe-merge-update.sh with REDACT_MAP_DIR set ...
umount "$REDACT_MAP_DIR" && rmdir "$REDACT_MAP_DIR"
What is sent to the claude CLI: Only the redacted content of conflicted files. Claude resolves conflict markers (<<<<<<<, =======, >>>>>>>) using only the Edit and Read tools — Bash is not granted, so the model cannot execute shell commands. The script itself runs git add -A && git commit --no-edit after claude finishes.
Backups in /tmp/safe-merge/backups/ contain only redacted content — they are created after redaction and never contain plaintext secrets.
.env files are never included in merge promptsThe validation phase runs pnpm install --ignore-scripts, pnpm build, and pnpm ui:build to verify the merge compiles.
pnpm install will download packages from the npm registry. This is network activity. --ignore-scripts is always passed to suppress preinstall/postinstall lifecycle hooks from running untrusted code.
Before proceeding past pre-flight:
package.json diffs in the pre-flight reportSKILL.md says "No Network Installs" — this refers to the skill package itself (no curl/wget/npm install in the skill's own setup). It does NOT mean the merge workflow avoids network; git fetch upstream and pnpm install both require network access.
Before ANY file edits, the skill creates a full backup of every conflicting file at /tmp/safe-merge/backups/ preserving directory structure. Backups are created after secret redaction — they never contain plaintext secrets. If the merge goes wrong, restore from backups and re-run secret restoration.
This skill contains no install scripts that download from external URLs. All files are local to the skill package. The only network activity is git fetch upstream and your normal model API calls.
Click the ↑ Update button in the topbar (right of Health pill).
/update — or ask: "run a safe merge update"
scripts/safe-merge-update.sh)The primary entry point. Run this directly — it handles the entire workflow end-to-end:
cd /path/to/openclaw
# Safe first step — shows divergence, makes no changes
REPO_DIR=. ./scripts/safe-merge-update.sh --dry-run
# Phase 1 — merge, build, restart gateway from safe-merge branch
# local-desktop-main is NOT touched; no remote push yet
REPO_DIR=. ./scripts/safe-merge-update.sh
# Manual mode — stop on conflicts for review instead of invoking claude
REPO_DIR=. ./scripts/safe-merge-update.sh --no-auto-resolve
# Resume after fixing conflicts manually (still Phase 1)
REPO_DIR=. ./scripts/safe-merge-update.sh --resume safe-merge-2026-03-02
# Phase 2 — after verifying the gateway is healthy, promote to local-desktop-main
REPO_DIR=. ./scripts/safe-merge-update.sh --promote safe-merge-2026-03-02
Two-phase design — local-desktop-main is never touched until the user confirms:
Phase 1 (automated):
local-desktop-main with a clean working tree--dry-run: fetches, shows upstream divergence + estimated conflict count, exits — no changesgit fetch --allsafe-merge-* branches (prevents accumulation)safe-merge-YYYY-MM-DD branchgit merge upstream/main --no-editclaude --allowedTools Edit,Read (Bash excluded), restores secrets, runs git commit — use --no-auto-resolve to stop for manual reviewpnpm build + pnpm ui:buildlocal-desktop-main is unchanged, no remote push yetPhase 2 (user-confirmed via --promote):
git push myfork safe-merge-YYYY-MM-DD:local-desktop-main --forcegit checkout local-desktop-main && git reset --hard safe-merge-YYYY-MM-DDgit branch -D safe-merge-YYYY-MM-DDRollback before confirming: gateway restart reverts to previous build; local-desktop-main was never touched.
Claude conflict resolution strategy (baked into the script):
Resume mode — for cases where Claude's auto-resolution needs a manual touch:
# Fix conflicts manually, then:
./scripts/safe-merge-update.sh --resume safe-merge-2026-03-02
# Skips merge, runs build → push → cleanup
scripts/preflight.sh — read-only analysis, produces JSON report at /tmp/safe-merge/preflight-report.jsonscripts/validate.sh — post-merge build + mustPreserve pattern checksscripts/merge-agent-prompt.md — prompt template for per-file conflict resolution (used when invoking Claude manually)scripts/redact-secrets.sh — secret detection and redaction before model transmissionlocal-desktop-main (your fork's primary branch)safe-merge-YYYY-MM-DD (created and destroyed per run)upstream/main (openclaw/openclaw official repo)myfork/local-desktop-mainscripts/preflight.sh)Run automatically when invoked. Produces a report at /tmp/safe-merge/preflight-report.json.
What it does:
git fetch upstream)git merge-treePre-flight report includes:
Environment variables:
REPO_DIR — Path to your OpenClaw repo (must be set explicitly)UPSTREAM_REMOTE — Upstream remote name (default: upstream)UPSTREAM_BRANCH — Upstream branch (default: main)The agent performs the actual merge and resolves conflicts:
git checkout -b safe-merge-YYYY-MM-DDgit merge upstream/main --no-commit --no-ffMERGE_MANIFEST.json for intent and strategy (keep-ours, accept-upstream, ai-merge)
c. Resolve: write the clean merged file preserving our customizations + upstream improvements
d. git add the resolved filemustPreserve patterns to confirm our custom code survivedscripts/redact-secrets.sh replaces secrets with [REDACTED_N] placeholdersKey principle: Always examine both sides of a conflict BEFORE attempting resolution. Understanding what upstream changed and what we customized is essential for correct merges.
scripts/validate.sh)After all conflicts are resolved:
pnpm install --ignore-scripts — install any new dependencies (lifecycle scripts suppressed)pnpm build — compile the gatewaypnpm ui:build — compile the Control UImustPreserve patterns from the manifest still existsafe-merge-YYYY-MM-DD) — never directly on mainmain and push:
git checkout main
git merge safe-merge-YYYY-MM-DD
git push origin main
| Variable | Default | Description |
|---|---|---|
REPO_DIR | (required, declared in metadata) | Path to your OpenClaw repository |
SAFE_MERGE_MODEL | (agent's current model) | Model override for conflict resolution |
UPSTREAM_REMOTE | upstream | Git remote name for upstream |
UPSTREAM_BRANCH | main | Upstream branch to merge from |
The model used for AI conflict resolution can be configured in two ways:
Via the UI modal — The update modal includes a "Merge Model" dropdown that lists all available models (same catalog as Agents → Model Selection). The selection is persisted in localStorage under the key openclaw-merge-model and survives page reloads. When a model is selected and "Run Safe Merge" is clicked, the merge prompt includes SAFE_MERGE_MODEL=<selected-model>.
Via environment variable — Set SAFE_MERGE_MODEL before invoking the skill (e.g., in the agent's env config or inline).
If neither is set, the skill uses the agent's currently configured primary model.
The dropdown appears on both the initial "Check for Updates" screen and the results screen, so you can change it at any point before starting the merge.
| File | Purpose |
|---|---|
SKILL.md | This file — skill instructions |
MERGE_MANIFEST.json | Protected files, intents, mustPreserve patterns |
scripts/preflight.sh | Pre-flight analysis (read-only, no modifications) |
scripts/validate.sh | Post-merge build and pattern validation |
scripts/merge-agent-prompt.md | Prompt template for per-file conflict resolution |
scripts/redact-secrets.sh | Secret detection and redaction before model transmission |
update-modal.ts | Reference copy of the UI update modal component (source of truth: ui/src/ui/views/update-modal.ts) |
references/bg-sessions-backend.ts | Reference: src/gateway/server-methods/bg-sessions.ts |
references/bg-sessions-controller.ts | Reference: ui/src/ui/controllers/bg-sessions.ts |
references/bg-sessions-views.ts | Reference: ui/src/ui/views/bg-sessions.ts |
A right-side drawer panel that lets you watch and talk to background/cron subagents in real-time, without leaving the Control UI.
openBgSessionsPanel(client, state)You (user injections)Agent (assistant responses)→ tool (tool calls with args)← result (tool results, truncated)system (compaction events)bgSessions.history every 3 seconds while panel is openchat.send RPC using the selected sessionKeyBackend (src/gateway/server-methods/bg-sessions.ts):
bgSessions.list — reads sessions.json for the default agent, filters for isolated/cron session keys (UUID format: agent:main:<uuid>), returns label, updatedAt, running status (lock file presence)bgSessions.history — loads the .jsonl transcript file via readSessionMessages(), simplifies to { role, text, timestamp, toolName } arraybgSessions.send is intentionally omitted — the UI calls chat.send directly with the target sessionKeyController (ui/src/ui/controllers/bg-sessions.ts):
openBgSessionsPanel(client, state) — sets panel open, fetches sessions, starts 3s pollcloseBgSessionsPanel(state) — clears panel open flag, stops pollloadBgSessions(client, state) — fetches session list via RPCloadBgSessionHistory(client, state, key) — fetches transcript via RPCselectBgSession(client, state, key) — changes selected session, reloads historysendBgMessage(client, state) — calls chat.send RPC with session key + message, refreshes historystartBgSessionsPolling(client, state) / stopBgSessionsPolling() — manage setIntervalView (ui/src/ui/views/bg-sessions.ts):
renderBgSessionsPanel(state, client) — full Lit HTML panel, rendered as overlay in app-render.tsbgSessionsPanelStyles — exported CSS (for reference; styles are embedded in the view's html template)State fields (added to AppViewState and OpenClawApp):
bgSessionsPanelOpen: boolean;
bgSessionsList: BgSession[] | null;
bgSessionsLoading: boolean;
bgSessionsSelectedKey: string | null;
bgSessionsHistory: BgMessage[] | null;
bgSessionsHistoryLoading: boolean;
bgSessionsInput: string;
bgSessionsSending: boolean;
The panel is rendered as the last overlay before the closing </div>:
${state.bgSessionsPanelOpen && state.client ? renderBgSessionsPanel(state, state.client) : nothing}
The update badge click handler also calls openBgSessionsPanel:
@click=${() => {
// ...existing update modal logic...
if ((state as any).client) { openBgSessionsPanel((state as any).client, state as any); }
}}
When merging future upstream changes, protect these new files:
"src/gateway/server-methods/bg-sessions.ts": {
"intent": "New RPC handlers for background session listing and history (bgSessions.list, bgSessions.history)",
"strategy": "keep-ours"
},
"ui/src/ui/controllers/bg-sessions.ts": {
"intent": "Controller for the background sessions panel — load, poll, send, select",
"strategy": "keep-ours"
},
"ui/src/ui/views/bg-sessions.ts": {
"intent": "Right-side drawer panel for viewing/talking to cron subagents",
"strategy": "keep-ours"
}
Also add these mustPreserve patterns to the relevant existing protected files:
ui/src/ui/app-render.ts: renderBgSessionsPanel, openBgSessionsPanel, bgSessionsPanelOpenui/src/ui/app.ts: bgSessionsPanelOpen, bgSessionsList, bgSessionsSelectedKeyui/src/ui/app-view-state.ts: bgSessionsPanelOpen, bgSessionsListClicking the topbar update button (in any state) opens an update modal with a guided flow:
update.checkUpstream RPC with force: true (bypasses cache)The modal is rendered by ui/src/ui/views/update-modal.ts and uses state properties updateModalState (closed/confirm/checking/result), upstreamDivergence, and mergeModel on the app component. A reference copy of the modal source is kept at skills/safe-merge-update/update-modal.ts.
The gateway runs two parallel checks:
update-startup.ts): Compares VERSION against npm latest. Used for standard installs.update.checkUpstream RPC): Runs git fetch upstream && git rev-list --count HEAD..upstream/main. Used for fork workflows. Result is cached for 5 minutes.For forks, the git check is authoritative — your local package.json version will often be ahead of npm (since you're building from source), so the npm check would incorrectly say "up to date."
After a successful merge, always:
pnpm ui:build — the Control UI is served from dist/control-ui/OPENCLAW_SERVICE_VERSION from the service unit:
NEW_VERSION=$(node -e "console.log(require('./package.json').version)")
sed -i "s/OPENCLAW_SERVICE_VERSION=.*/OPENCLAW_SERVICE_VERSION=$NEW_VERSION/" ~/.config/systemd/user/openclaw-gateway.service
systemctl --user daemon-reload
openclaw gateway restart — pick up the new build.strict() to schemasrm -rf /tmp/safe-merge/ — while backups are redacted, remove them when no longer neededscripts/safe-merge-update.sh — end-to-end automated merge pipelineorigin/main → upstream/main (now pulls real OpenClaw updates)local-desktop-main (was main)safe-merge-* branches on each run (prevents infinite accumulation)myfork/local-desktop-main, deletes temp branch, restarts gateway--resume flag: skip merge, jump straight to build → push → cleanup (for post-manual-fix)hostinger.ts, plugins-ui.ts, memory.ts as protected filesbgSessions.list and bgSessions.history RPC handlersbgSessionsPanelOpen, bgSessionsList, bgSessionsSelectedKey, bgSessionsHistory, bgSessionsInput, bgSessionsSending, bgSessionsHistoryLoading, bgSessionsLoadingmustPreserve patternsConflicts resolved in: app-render.helpers.ts, app-render.ts, app-view-state.ts, app.ts. Key resolutions:
app-render.helpers.ts: kept renderContextGauge, merged hideCron + sessionsHideCron from upstream, merged countHiddenCronSessions, preserved renderRecentArchivedOptionsapp-render.ts: kept our nav imports (getDynamicTabGroups, Jarvis/mode/usage); added upstream's resolveConfiguredCronModelSuggestionsapp-view-state.ts: kept sessionsAgentFilter + added sessionsHideCron from upstreamapp.ts: kept session history fields + added sessionsHideCron = true default from upstreamUpstream added .strict() to DiscordVoiceSchema, rejecting keys our fork previously supported. Fix: remove unsupported keys from config, or add them back to the schema.
pnpm build builds the gateway but NOT the Control UI. UI needs separate pnpm ui:build. The validate script now includes this.
Git auto-merge kept both our extracted const AND upstream's inline block. Fix: manual dedup during AI conflict resolution.
Our fork adds http://localhost:* (Jarvis voice agent) and https://api.openai.com (Realtime API) to the CSP connect-src directive in control-ui-csp.ts. Upstream only has ws: wss:. On merge, always keep our extensions — they're required for voice features.
Merge branches should be date-stamped: safe-merge-YYYY-MM-DD. This makes it easy to identify merge attempts and clean up old branches.
Even files that auto-merge without conflicts can lose custom code if upstream refactors the surrounding context. After merge, always verify mustPreserve patterns exist in auto-merged protected files — don't just trust git's auto-merge.
Upstream may add new dependencies. When 162 commits are merged, pnpm install downloaded 51 new packages. This is expected but worth noting in the merge report.
/tmp/safe-merge/backups/git fetch and pnpm install