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.
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.
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.
-
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 loginas the same user the OpenClaw gateway runs under. Stored auth lives at~/.codex/auth.json. -
codexplugin enabled in OpenClaw.openclaw plugins enable codex openclaw plugins list | grep -i codex # expect: enabled
-
App-server command pinned to an absolute path so the gateway doesn't PATH-resolve a different binary. Edit
~/.openclaw/openclaw.json: -
(Linux/root only) The Claude Code CLI ≥ 2.1.x refuses
--dangerously-skip-permissionswhen invoked as root. Sol'sclaude-cliruntime spawns it with that flag, which means rollback would silently fail unless you bypass the check. SetIS_SANDBOX=1in 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
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)
PYThe 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.
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.5Don'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}')
PYDecisive 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.
- 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
- Slash commands defined under
- Tone/voice: gpt-5.5 ≠ Sonnet 4.6. Voice consistency drops.
- Reply quality clearly worse than Claude (hallucinations in domains the agent used to handle correctly)
- Tool calls misfire (wrong args, ignored schemas, malformed responses)
- Skills relying on Claude-Code-specific conventions silently break
- Voice feels "off-brand" enough that users notice
- ChatGPT subscription quota burning faster than expected
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.
- Codex CLI install (
/usr/bin/codex) — leaves it in place, idle. codexandopenai-codexplugin enables — leaves them.- Test-only
codex-testagent if you created one. - The
IS_SANDBOX=1env on systemd (this is what makesclaude-cliwork as root in the first place; don't remove it or rollback breaks too). - The
~/.codex/auth.jsonChatGPT credentials.
Re-applying the switch later is two openclaw config set commands plus a
systemd restart — keep the snapshot file around.
openai-codex/gpt-5.5≠ native Codex harness. That model id with the embedded PI runner uses ChatGPT OAuth againstapi.openai.comdirectly, which 401s. You wantopenai/gpt-5.5+agentRuntime.id: codex.openclaw models auth login --provider openai-codexis sufficient for the PI route only. For the native harness you also needcodex login(codex CLI's own auth, separate from openclaw's auth-profiles.json).- Don't confuse openclaw's "default" model with what Sol actually runs.
With
agentRuntime.id: claude-cli, Sol always uses Anthropic regardless ofagents.defaults.model.primary— that key affects openclaw's own internal LLM calls, not the agent's user-facing replies. - Trust the trace, not the self-report. Sol will say she's on Claude
because her
IDENTITY.mdsays so. UpdateIDENTITY.mdif accurate self-reporting matters. - Don't restart a production gateway without explicit approval for that exact restart. The switch + rollback both require restarts; schedule them.
- A clean
openclaw doctorrun is not proof the harness works. It only means config parses. Smoke-test with a real agent invocation and checkagentHarnessIdin the JSON output.
- 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
{ "plugins": { "entries": { "codex": { "enabled": true, "config": { "appServer": { "command": "/usr/bin/codex", "args": ["app-server", "--listen", "stdio://"] } } } } } }