Skip to content

Instantly share code, notes, and snippets.

@adg29
Created May 2, 2026 03:42
Show Gist options
  • Select an option

  • Save adg29/97d62fafb21dcd4c89aa3dffcc307fcb to your computer and use it in GitHub Desktop.

Select an option

Save adg29/97d62fafb21dcd4c89aa3dffcc307fcb to your computer and use it in GitHub Desktop.
OpenClaw + Codex native harness switch & rollback runbook (Sol bot)

OpenClaw + Codex switch & rollback runbook

Switching the OpenClaw agent's runtime from Anthropic Claude (via claude-cli) to OpenAI Codex (via the native codex harness), with a one-command rollback.

Tested on: OpenClaw 2026.4.25, Codex CLI 0.128.0, Linux/x86_64, root.

Why

Move bot inference off Anthropic Pro/Max (which charges metered "extra usage" once subscription quota is exhausted) and onto a ChatGPT subscription via the Codex OAuth flow + native Codex harness. Skills, memory files, system prompt, and Slack/Telegram routing all stay runtime-agnostic; only the LLM substrate changes.

The two-runtime distinction (read this first)

These look similar but route differently — getting them confused is the most common pitfall.

Setup Model id Runtime Auth path Cost
OpenAI Codex OAuth via PI runner openai-codex/gpt-5.5 (any) OpenClaw's openai-codex plugin hits api.openai.com/v1/responses directly — fails 401 with ChatGPT-only OAuth
Native Codex harness (this runbook) openai/gpt-5.5 codex Codex CLI's own codex login ChatGPT subscription

If you set openai-codex/gpt-5.5 as the agent's model and route via the embedded PI path, the dispatcher will call https://api.openai.com/v1/responses and a ChatGPT-flavored OAuth token (the kind openclaw models auth login --provider openai-codex produces) is rejected with 401 Unauthorized: Missing bearer or basic authentication. The fix is not a different model id — it's switching the runtime to codex and using openai/gpt-5.5 as the model.

Prerequisites

  1. Codex CLI installed and auth'd. Verify the absolute path the gateway will launch:

    which codex && codex --version
    codex login --device-auth        # paste the URL + code in any browser
    codex login status               # expect: "Logged in using ChatGPT"

    Run codex login as the same user the OpenClaw gateway runs under. Stored auth lives at ~/.codex/auth.json.

  2. codex plugin enabled in OpenClaw.

    openclaw plugins enable codex
    openclaw plugins list | grep -i codex   # expect: enabled
  3. App-server command pinned to an absolute path so the gateway doesn't PATH-resolve a different binary. Edit ~/.openclaw/openclaw.json:

    {
      "plugins": {
        "entries": {
          "codex": {
            "enabled": true,
            "config": {
              "appServer": {
                "command": "/usr/bin/codex",
                "args": ["app-server", "--listen", "stdio://"]
              }
            }
          }
        }
      }
    }
  4. (Linux/root only) The Claude Code CLI ≥ 2.1.x refuses --dangerously-skip-permissions when invoked as root. Sol's claude-cli runtime spawns it with that flag, which means rollback would silently fail unless you bypass the check. Set IS_SANDBOX=1 in the gateway's systemd unit so both runtimes coexist:

    # ~/.config/systemd/user/openclaw-gateway.service
    Environment=IS_SANDBOX=1
    systemctl --user daemon-reload
    systemctl --user restart openclaw-gateway
    cat /proc/$(pgrep -f openclaw-gateway | head -1)/environ \
      | tr '\0' '\n' | grep IS_SANDBOX        # expect: IS_SANDBOX=1

Pre-switch snapshot (rollback target)

Capture the current "agent on Claude" state before any edits. Without this, rollback is a fuzzy reconstruction.

SNAPDIR=/root/clawd/skills/usage-monitor
mkdir -p "$SNAPDIR"
cp ~/.openclaw/openclaw.json "$SNAPDIR/.openclaw.json.pre-codex-switch.bak"
chmod 600 "$SNAPDIR/.openclaw.json.pre-codex-switch.bak"

python3 - <<'PY'
import json
d = json.load(open('/root/.openclaw/openclaw.json'))
sol = next((a for a in d['agents']['list'] if a.get('id') == 'main'), None)
out = {
    'sol_model_explicit': sol.get('model'),
    'sol_runtime_explicit': sol.get('agentRuntime', {}).get('id'),
    'defaults_model': d['agents']['defaults']['model']['primary'],
    'defaults_runtime': d['agents']['defaults']['agentRuntime']['id'],
}
json.dump(out, open('/root/clawd/skills/usage-monitor/.sol-pre-codex-state.json','w'), indent=2)
print(out)
PY

The trick used here: leave agents.defaults pristine and put the Codex overrides on agents.list[0] (the agent you're switching, here main). Rollback then reduces to "delete the per-agent overrides" — a one-line edit instead of a multi-key revert.

The switch

openclaw config set 'agents.list[0].model' openai/gpt-5.5
openclaw config set 'agents.list[0].agentRuntime.id' codex

# Make sure openai/gpt-5.5 is in the configured-models map
python3 - <<'PY'
import json
p = '/root/.openclaw/openclaw.json'
d = json.load(open(p))
d['agents']['defaults']['models'].setdefault('openai/gpt-5.5', {})
json.dump(d, open(p, 'w'), indent=2)
PY

systemctl --user restart openclaw-gateway
sleep 4
openclaw agents list | head -10        # expect: Model: openai/gpt-5.5

Verification (decisive fields)

Don't trust the agent's text reply ("I'm running on Claude") — the agent parrots its IDENTITY.md, not its actual runtime. Inspect the dispatch trace:

openclaw agent --agent main \
  --message "Reply with one sentence: what model are you running on?" \
  --json > /tmp/smoke.json

python3 - <<'PY'
import json
d = json.load(open('/tmp/smoke.json'))
def find(o, k):
    if isinstance(o, dict):
        if k in o: return o[k]
        for v in o.values():
            r = find(v, k)
            if r is not None: return r
    elif isinstance(o, list):
        for v in o:
            r = find(v, k)
            if r is not None: return r
for k in ['agentHarnessId', 'winnerProvider', 'winnerModel', 'fallbackUsed']:
    print(f'{k}: {find(d, k)!r}')
PY

Decisive fingerprint:

agentHarnessId: 'codex'
winnerProvider: 'openai'
winnerModel:    'gpt-5.5'
fallbackUsed:   False

Additional supporting evidence:

# Codex app-server child processes spawn while harness turns are running
pgrep -af 'codex.*app-server|codex.*stdio'

If agentHarnessId is anything other than codex, or if fallbackUsed is True, the request did not stay on the Codex harness — investigate before declaring success.

What happens to existing sessions

  • Slack/Telegram thread transcripts: preserved by the platform, unchanged.
  • OpenClaw session store (~/.openclaw/agents/main/sessions/sessions.json): preserved as records, but each session's agent-side working memory effectively resets on next turn — Claude Code session state lives at ~/.claude/projects/... while Codex's lives at ~/.codex/sessions/..., separate stores. When the next turn fires under Codex, the harness sees only what OpenClaw passes (the channel transcript), not the previous Claude scratchpad.
  • Skills, MEMORY.md, SOUL.md, AGENTS.md: workspace-relative, runtime-agnostic. Codex reads them as instructions just like Claude did.
  • Things that may behave differently:
    • Slash commands defined under ~/.claude/commands/ (Claude-Code-specific)
    • Hooks in ~/.claude/settings.json (Claude-Code-specific)
    • Sub-agent invocations (Plan, Explore, etc. — Claude-Code-specific)
    • Skills that shell out to claude -p ... continue using Claude API directly, independent of Sol's runtime
  • Tone/voice: gpt-5.5 ≠ Sonnet 4.6. Voice consistency drops.

Rollback

Symptoms that warrant rollback

  1. Reply quality clearly worse than Claude (hallucinations in domains the agent used to handle correctly)
  2. Tool calls misfire (wrong args, ignored schemas, malformed responses)
  3. Skills relying on Claude-Code-specific conventions silently break
  4. Voice feels "off-brand" enough that users notice
  5. ChatGPT subscription quota burning faster than expected

One-command rollback

Save this as rollback-sol-runtime.sh, chmod +x, and run when needed:

#!/bin/bash
# Strip Sol's explicit Codex overrides → Sol falls back to defaults (Claude).
set -uo pipefail

CONFIG=~/.openclaw/openclaw.json
cp "$CONFIG" "$CONFIG.bak-rolled-back-$(date +%Y%m%d-%H%M%S)"

python3 - <<'PY'
import json
p = '/root/.openclaw/openclaw.json'
d = json.load(open(p))
sol = next(a for a in d['agents']['list'] if a.get('id') == 'main')
removed = {}
if 'model' in sol: removed['model'] = sol.pop('model')
if 'agentRuntime' in sol: removed['agentRuntime'] = sol.pop('agentRuntime')
json.dump(d, open(p, 'w'), indent=2)
print('Removed:', removed)
print('Sol now inherits defaults:',
      d['agents']['defaults']['model']['primary'], '+',
      d['agents']['defaults']['agentRuntime']['id'])
PY

systemctl --user restart openclaw-gateway
sleep 4
openclaw agents list | grep -A6 'main (default)'

Effect: next message lands on Claude. In-flight Codex requests finish on Codex. Takes ~5 seconds end-to-end.

What rollback does NOT undo

  • Codex CLI install (/usr/bin/codex) — leaves it in place, idle.
  • codex and openai-codex plugin enables — leaves them.
  • Test-only codex-test agent if you created one.
  • The IS_SANDBOX=1 env on systemd (this is what makes claude-cli work as root in the first place; don't remove it or rollback breaks too).
  • The ~/.codex/auth.json ChatGPT credentials.

Re-applying the switch later is two openclaw config set commands plus a systemd restart — keep the snapshot file around.

Common gotchas

  1. openai-codex/gpt-5.5 ≠ native Codex harness. That model id with the embedded PI runner uses ChatGPT OAuth against api.openai.com directly, which 401s. You want openai/gpt-5.5 + agentRuntime.id: codex.
  2. openclaw models auth login --provider openai-codex is sufficient for the PI route only. For the native harness you also need codex login (codex CLI's own auth, separate from openclaw's auth-profiles.json).
  3. Don't confuse openclaw's "default" model with what Sol actually runs. With agentRuntime.id: claude-cli, Sol always uses Anthropic regardless of agents.defaults.model.primary — that key affects openclaw's own internal LLM calls, not the agent's user-facing replies.
  4. Trust the trace, not the self-report. Sol will say she's on Claude because her IDENTITY.md says so. Update IDENTITY.md if accurate self-reporting matters.
  5. Don't restart a production gateway without explicit approval for that exact restart. The switch + rollback both require restarts; schedule them.
  6. A clean openclaw doctor run is not proof the harness works. It only means config parses. Smoke-test with a real agent invocation and check agentHarnessId in the JSON output.

Reference paths

  • Config: ~/.openclaw/openclaw.json
  • Pre-switch snapshot: ~/clawd/skills/usage-monitor/.openclaw.json.pre-codex-switch.bak
  • Rollback script: ~/clawd/skills/usage-monitor/rollback-sol-runtime.sh
  • Codex CLI: /usr/bin/codex (npm-global symlink)
  • Codex auth: ~/.codex/auth.json
  • OpenClaw auth profiles: ~/.openclaw/agents/main/agent/auth-profiles.json
  • Gateway log: /tmp/openclaw/openclaw-YYYY-MM-DD.log
  • Systemd unit: ~/.config/systemd/user/openclaw-gateway.service
#!/bin/bash
# rollback-sol-runtime.sh — revert Sol from Codex back to Claude.
#
# Use when:
# - Sol's voice has degraded enough that you don't want it for another message
# - Skills/features that worked on Claude are silently broken on Codex
# - You're about to demo Sol and need known-good behavior
# - Cost calc went wrong and you actually want Anthropic burn back
#
# Effect:
# - Removes the per-agent model + agentRuntime override on `main` (Sol),
# so Sol falls back to defaults: anthropic/claude-sonnet-4-6 + claude-cli.
# - Restarts the gateway via systemd.
# - Verifies and reports Sol's effective model.
#
# It does NOT:
# - Disable the codex plugin
# - Remove the codex-test agent
# - Touch the openai-codex OAuth profile
# - Delete the saved snapshot (so you can re-roll-forward later)
set -uo pipefail
CONFIG=/root/.openclaw/openclaw.json
SNAPSHOT=/root/clawd/skills/usage-monitor/.openclaw.json.pre-codex-switch.bak
STATE=/root/clawd/skills/usage-monitor/.sol-pre-codex-state.json
echo "Pre-switch state recorded:"
[ -f "$STATE" ] && cat "$STATE" || echo " (no state file — falling back to claude-cli + claude-sonnet-4-6)"
echo
# Backup the current (Codex) config so we can re-apply later if desired
cp "$CONFIG" "$CONFIG.bak-rolled-back-$(date +%Y%m%d-%H%M%S)"
# Strip the explicit overrides from agents.list[0] so Sol inherits defaults again
python3 - <<'PY'
import json
p = "/root/.openclaw/openclaw.json"
d = json.load(open(p))
sol = next(a for a in d["agents"]["list"] if a.get("id") == "main")
removed = {}
if "model" in sol:
removed["model"] = sol.pop("model")
if "agentRuntime" in sol:
removed["agentRuntime"] = sol.pop("agentRuntime")
json.dump(d, open(p, "w"), indent=2)
print(f"Removed Sol's explicit overrides: {removed}")
print(f"Sol now inherits defaults: model={d['agents']['defaults']['model']['primary']}, runtime={d['agents']['defaults']['agentRuntime']['id']}")
PY
# Restart gateway via systemd
echo
echo "Restarting openclaw-gateway..."
systemctl --user restart openclaw-gateway
sleep 4
# Verify
echo "--- Sol's effective state ---"
openclaw agents list 2>&1 | grep -A6 'main (default)' | head -8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment