OpenClaw version: 2026.2.13 (203b5bd)
Setup: Claude Max subscription, using OAuth token (sk-ant-oat01-...) in auth-profiles.json. No API key, no env var.
What works:
• openclaw agent --local --session-id <id> --message "hello" → responds correctly
• openclaw models status shows auth is valid, usage tracking works (5h 85% left)
• Gateway logs show agent runs completing successfully with no errors (~20s, aborted=false)
What doesn't work:
• TUI (openclaw tui) → sends message, agent runs, shows "(no output)", tokens stay at 0/200k
• Dashboard (Chat UI) → sends message, agent name + timestamp appear, but response body is empty
Logs show: The embedded agent cycles through multiple start/end pairs and completes without any errors, auth failures, or timeouts. No 401/403 in logs. The run finishes with run_completed status but no text is rendered.
Suspicion: The gateway path isn't capturing/streaming the response body from the embedded agent when using OAuth tokens. The --local CLI flag bypasses the gateway and works fine, so the issue seems to be in how the gateway relays the agent response to connected clients (TUI/Dashboard).
Steps to reproduce:
1 Configure auth-profiles.json with a Claude Code OAuth token (sk-ant-oat01-...)
2 openclaw gateway restart
3 openclaw tui → send any message → "(no output)"
4 openclaw agent --local --session-id <session> --message "hello" → works
#Bug: Gateway returns empty response body in TUI and Dashboard (OAuth token auth)
1 messages · Page 1 of 1 (latest)
A couple of quick isolations that usually make “(no output)” bugs go from mysterious → obvious:
1) Does the gateway CLI path also lose the reply?
Run the same thing without --local (so it goes through the Gateway, but avoids TUI/Dashboard rendering):
openclaw agent --session-id <id> --message "hello"
- If this is also empty → the Gateway/embedded runner is producing (or storing) an assistant message with no renderable text.
- If this prints fine → the bug is likely in the TUI/Dashboard event/message parsing.
2) Check what the assistant message “content” actually looks like in the transcript
On the gateway host, look at the last assistant line in the session transcript:
tail -n 20 ~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl
You’re looking for the assistant message’s content blocks. If you see something like:
"content":[{"type":"output_text","text":"..."}]
that would explain the symptom: the current TUI formatter only renders blocks where type === "text" (so output_text ends up as empty UI). (Same likely applies to the Dashboard renderer.)
If you can paste just one redacted assistant message JSON (remove token/PII), it’ll be pretty clear which side is broken.
3) Quick workaround test: disable streaming for the model
If the issue is in the streaming/subscribe path, forcing non-streaming often makes the final text show up:
{
agents: {
defaults: {
models: {
"<your-model-id-here>": { streaming: false }
}
}
}
}
Restart gateway and retry TUI/Dashboard.
4) If you file a GitHub issue, the most useful attachments are:
- output of
openclaw --version - whether
openclaw agent --session-id ...(no--local) is empty or not
- 1–2 redacted transcript lines showing the assistant message
contentshape - optionally raw stream log (redact!): set
OPENCLAW_RAW_STREAM=1and reproduce (writes~/.openclaw/logs/raw-stream.jsonl)
If you drop the transcript snippet here, I can tell you immediately whether it’s the output_text-vs-text block type mismatch or something else.
Hey Krill, followed your steps. Here's what I found:
Step 1: Gateway CLI path also loses the reply openclaw agent --session-id <id> --message "hello" (no --local) → "Connection error." every time. So yes, the gateway path is broken, not just the TUI/Dashboard rendering.
Step 2: Transcript content shape The successful --local responses have normal content blocks:
json
"content":[{"type":"text","text":"I'm running on Claude Opus 4.0..."}]
"stopReason":"stop"
"usage":{"input":3,"output":25,...}
The gateway responses are all empty:
json
"content":[]
"stopReason":"error"
"usage":{"input":0,"output":0,...}
"errorMessage":"Connection error."
Zero tokens, 4 retries with backoff, never reaches the API.
Important context: I'm not using a standard API key. I'm using a Claude Max OAuth token (sk-ant-oat01-...) stored in auth-profiles.json. The embedded runner (--local) handles it correctly, but the gateway can't connect at all with it. I previously had a standard API key and everything worked — this started when I switched to the OAuth token.
Step 3: Skipped — streaming isn't relevant since the gateway fails before getting any response.
That’s a really good find — the transcript proves the embedded run is failing before it ever gets a response (stopReason:"error", usage: 0, errorMessage:"Connection error."). So this isn’t a UI/TUI rendering problem anymore; it’s “Gateway process cannot reach Anthropic successfully with the credential it’s selecting”.
Given --local works on the same session/model, the two most common causes are:
- Gateway is not using the same auth store / agentDir / stateDir as your CLI
--localrun, or - Gateway is selecting a different Anthropic profile than the setup-token one (e.g. a leftover
oauthprofile gets preferred over yourtokenprofile).
A. Can you paste this (safe, no secrets) output?
Run on the gateway host:
openclaw models auth order get --provider anthropic --json
That will show:
agentIdagentDir
authStorePath- whether there’s an Anthropic order override
This tells us definitively which auth-profiles.json the gateway should be reading.
B. Force Anthropic to use the setup-token profile (quick workaround)
If you have multiple Anthropic profiles in auth-profiles.json, OpenClaw will prefer oauth over token by default. To eliminate that variable, set an explicit order override to your setup-token profile id:
openclaw models auth order set --provider anthropic <profileId>
Example (very common):
openclaw models auth order set --provider anthropic anthropic:default
If you’re not sure what the profile ids are, you can list just the keys (no token values) from the auth store file path you saw in step A.
C. Get the real underlying connection failure (DNS/TLS/etc)
“Connection error.” is almost certainly an Anthropic SDK “APIConnectionError” wrapper; the useful bit is the underlying cause (ENOTFOUND, ECONNRESET, TLS, proxy, etc). To surface it:
- raise file log level:
openclaw config set logging.level debug
openclaw gateway restart
- reproduce once, then grab the gateway log lines around the failure:
openclaw logs --follow
If you paste ~30 lines around the error (redact tokens), we can tell whether this is DNS/egress/TLS vs “wrong credential selected”.
One key question too: is --local being run on the same machine as the Gateway, or are you pointing your CLI at a remote gateway? That single detail changes the diagnosis a lot.
Here are the results from your steps:
Step A: Auth order output:
{
"agentId": "main",
"agentDir": "/Users/jonesy/.openclaw/agents/main/agent",
"provider": "anthropic",
"authStorePath": "~/.openclaw/agents/main/agent/auth-profiles.json",
"order": null
}
Single profile (anthropic:default), no order override.
Step C: Set logging.level to debug, restarted gateway, reproduced. Here are ~30 lines around the gateway failure (tokens redacted), followed by the successful --local run for comparison:
Gateway path (failed):
01:06:04 info gateway/ws ⇄ res ✓ agent 65ms runId=f6d3c4ca-d6ad-4ab8-9509-bf5b936782f2 conn=fc0ba23d…6cd5 id=32296e7c…af80
01:06:04 debug agent/embedded embedded run start: runId=f6d3c4ca-d6ad-4ab8-9509-bf5b936782f2 sessionId=0df535eb-e8ea-41e3-8fac-deaa8e0972c8 provider=anthropic model=claude-opus-4-6 thinking=low messageChannel=whatsapp
01:06:04 debug diagnostic session state: sessionId=0df535eb prev=idle new=processing reason="run_started" queueDepth=0
01:06:04 debug diagnostic run registered: sessionId=0df535eb totalActive=1
01:06:04 debug agent/embedded embedded run prompt start: runId=f6d3c4ca sessionId=0df535eb
01:06:04 debug agent/embedded [context-diag] pre-prompt: sessionKey=agent:main:main messages=128 roleCounts=assistant:97,toolResult:1,user:30 historyTextChars=3137 systemPromptChars=31648 promptChars=34 provider=anthropic/claude-opus-4-6
01:06:24 debug agent/embedded embedded run prompt end: runId=f6d3c4ca sessionId=0df535eb durationMs=20236
01:06:24 debug diagnostic session state: sessionId=0df535eb prev=processing new=idle reason="run_completed" queueDepth=0
01:06:24 debug diagnostic run cleared: sessionId=0df535eb totalActive=0
01:06:24 debug agent/embedded embedded run done: runId=f6d3c4ca sessionId=0df535eb durationMs=20297 aborted=false
01:06:24 debug diagnostic lane task done: lane=main durationMs=20301 active=0 queued=0
01:06:24 debug diagnostic lane task done: lane=session:agent:main:main durationMs=20305 active=0 queued=0
No response text logged. Transcript shows "content":[], "stopReason":"error", "errorMessage":"Connection error.", zero tokens. No ENOTFOUND/ECONNRESET/TLS detail surfaced even at debug level.
--local path (worked) — same session, 2 minutes later:
01:08:34 debug diagnostic lane dequeue: lane=session:agent:main:main waitMs=8 queueSize=0
01:08:34 debug agent/embedded embedded run start: runId=0df535eb sessionId=0df535eb provider=anthropic model=claude-opus-4-6 thinking=low messageChannel=unknown
01:08:35 debug diagnostic session state: sessionId=0df535eb prev=idle new=processing reason="run_started" queueDepth=0
01:08:35 debug diagnostic run registered: sessionId=0df535eb totalActive=1
01:08:35 debug agent/embedded embedded run prompt start: runId=0df535eb sessionId=0df535eb
01:08:35 debug agent/embedded [context-diag] pre-prompt: sessionKey=agent:main:main messages=133 roleCounts=assistant:101,toolResult:1,user:31 historyTextChars=3171 systemPromptChars=31468 promptChars=5 provider=anthropic/claude-opus-4-6
01:08:38 debug agent/embedded embedded run prompt end: runId=0df535eb sessionId=0df535eb durationMs=3289
01:08:38 debug diagnostic session state: sessionId=0df535eb prev=processing new=idle reason="run_completed" queueDepth=0
01:08:38 debug diagnostic run cleared: sessionId=0df535eb totalActive=0
01:08:38 debug agent/embedded embedded run done: runId=0df535eb sessionId=0df535eb durationMs=4321 aborted=false
Response returned successfully in ~3 seconds, single agent cycle.
Summary:
- Gateway:
messageChannel=whatsapp, 4 retry cycles, ~20s, Connection error, zero tokens --local:messageChannel=unknown, 1 cycle, ~3s, success- Same token (sk-ant-oat01-..., Claude Max OAuth), same session, same machine (MacBook Air, no remote gateway)
- Auth is a Claude Max subscription OAuth token, not a standard API key
Should I try OPENCLAW_RAW_STREAM=1 to capture the actual HTTP-level failure?
OPENCLAW_RAW_STREAM=1 probably won’t help here — it logs the assistant text stream events, and in your failing case you’re never getting any assistant deltas back anyway.
What you want is the promptError that gets swallowed right now (the runner sets promptError = err but doesn’t log it at debug), and there is a built-in hook for that:
Use the Anthropic payload/error logger (best next step)
This will write a JSONL file that includes:
stage:"request"entries (full payload — sensitive!)- a
stage:"usage"entry that includes the error string even when usage is 0 (this is the money line)
Run locally and reproduce with a “safe” prompt/session (so you don’t leak real convo into the payload log):
openclaw gateway stop # if you run it as a service
OPENCLAW_ANTHROPIC_PAYLOAD_LOG=1 \
OPENCLAW_ANTHROPIC_PAYLOAD_LOG_FILE=~/.openclaw/logs/anthropic-payload.jsonl \
openclaw gateway --verbose
Then in another terminal, trigger the failing path with a fresh session:
openclaw agent --session-id probe-conn --message "hi"
Now inspect only the error line (don’t post the request payloads):
tail -n 50 ~/.openclaw/logs/anthropic-payload.jsonl | grep '"stage":"usage"'
If you paste just that one stage:"usage" JSON object (redacting as needed), it should finally show the underlying cause (timeout, TLS, ECONNRESET, blocked by firewall/Little Snitch, etc.).
Extra quick isolation
If it works when you run the gateway in the foreground (command above), but fails when running as the background LaunchAgent, that strongly points to daemon environment / network permission differences (macOS firewall tools love doing this).
Post that stage:"usage" line and whether “foreground gateway” changes behavior, and we’ll be able to narrow it down fast.
Found the root cause. After clearing cooldown and re-authenticating with a fresh OAuth token, the gateway gives:
HTTP 401 authentication_error: invalid x-api-key (request_id: req_011CYAVcQUj8VDS1b2CywyuU)
The gateway is sending my OAuth token (sk-ant-oat01-...) as an x-api-key header, but OAuth tokens need to go in the Authorization: Bearer header. The --local embedded runner handles this correctly (uses the right auth header for OAuth tokens), but the gateway path treats all tokens as standard API keys.
This explains the full chain:
--localalways worked because the embedded runner sends OAuth tokens correctly- Gateway path always failed with "Connection error" (which was a wrapped 401)
- The 401s triggered cooldown, which cascaded to fallback models failing too
The earlier "Connection error" mystery was just this same 401 being swallowed before we enabled payload logging.
Setup: Claude Max subscription, OAuth token from Claude Code (sk-ant-oat01-...), stored as type: "token" in auth-profiles.json. No standard API key.
Version: OpenClaw 2026.2.13 (203b5bd), everything local on MacBook Air.
Payload log stage:"usage" line:
{"runId":"8cdbd3f7-e7b4-4ac2-91d3-469bc43b5e1c","sessionId":"probe-conn2","provider":"anthropic","modelId":"claude-opus-4-6","modelApi":"anthropic-messages","workspaceDir":"/Users/jonesy/.openclaw/workspace","ts":"2026-02-15T20:27:29.342Z","stage":"usage","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}}}
That 401 is the key clue — but one important nuance from the docs/codebase:
sk-ant-oat01-...is treated by OpenClaw as an Anthropic setup-token (subscription auth), and it’s intentionally used “like an API key” (i.e. the “invalid x-api-key” error is consistent with the expected header style for setup-tokens in OpenClaw).- OpenClaw explicitly does not reuse Claude Code CLI OAuth tokens for gateway runs; the supported subscription path is setup-token (or a real Anthropic API key).
Docs:
- https://docs.openclaw.ai/providers/anthropic
- https://docs.openclaw.ai/help/faq#how-does-anthropic-setuptoken-auth-work
- https://docs.openclaw.ai/help/faq#do-you-support-claude-subscription-auth-claude-pro-or-max
So the immediate interpretation is:
- Gateway/TUI/Dashboard “(no output)” was the UI symptom of an upstream auth failure.
- The remaining bug is that the gateway client surfaces it badly (empty body / 0 tokens / looks like a successful run), instead of showing the 401 payload.
Two practical next steps:
- Make sure you’re using a setup-token generated via
claude setup-token, and paste it via the supported command (instead of hand-editing):
openclaw models auth paste-token --provider anthropic
# or run on the gateway host:
openclaw models auth setup-token --provider anthropic
- Please file a GitHub issue for the “401 gets swallowed → empty response in TUI/Dashboard” behavior, including the
request_idyou captured and your repro steps:
https://github.com/openclaw/openclaw/issues
If you paste your (redacted) auth-profiles.json entry shape for the anthropic profile (just the fields/type/provider/whether it has expires, not the secret), I can sanity-check that it matches what OpenClaw expects for setup-token auth.
ok that worked. So what was the issue in the end?