#1587 · Transcription often fails with HTTP 403 due to cloudflare when using codex subscription
TL;DR
Plain-language framing. When bb is configured with the default transcription model codex/gpt-transcribe and the local Codex CLI is logged in with a ChatGPT subscription (~/.codex/auth.json has auth_mode: "chatgpt"), voice input (bb voice transcribe file.mp3 or the microphone button in the prompt box) is executed by the host daemon, which POSTs the audio to https://chatgpt.com/backend-api/transcribe with the subscription's bearer token. That URL is not a Codex endpoint: it is the ChatGPT web app's transcription endpoint, and it sits behind Cloudflare bot management (a web application firewall, WAF, rule that issues a "managed challenge"). The Codex endpoint the daemon uses for the other helper calls, https://chatgpt.com/backend-api/codex/responses, is not behind that challenge.
When Cloudflare decides to challenge a request it answers HTTP 403 with the header cf-mitigated: challenge and a "Just a moment…" HTML page that only a JavaScript-running browser can solve. The daemon already knows about this: it re-sends the request once with the Cloudflare cookies from the challenge response. If the retry is challenged too, the daemon gives up with the generic, permanent error code codex_request_failed whose message embeds the first 400 characters of the challenge page. The server treats that code as non-transient (no second attempt, no fallback), so the user sees Error: HTTP 502: Codex transcription request failed with HTTP 403: <html> <head> <meta name="viewport" ….
Whether a given request is challenged is decided by Cloudflare per IP / TLS fingerprint / headers and fluctuates. On my network the daemon's exact header set (Authorization + chatgpt-account-id + originator: bb + User-Agent: bb-host-daemon) passed 10/10 times, while the same request with only Authorization was challenged 7/10 times, and unauthenticated probes were challenged 10/10 for every User-Agent tried (all of them pass the /backend-api/codex/responses path). The reporters ("usually fails on my machine", "same here") are presumably on networks where the same daemon request is scored badly (inferred, not observed: Cloudflare's per-IP/fingerprint scoring is opaque and the issue does not say). I therefore could not trigger a real challenge with the real headers from here; I reproduced the failure end to end by feeding the daemon the real captured challenge response through a Node preload, and I verified the mechanism live with curl. The issue's guess that an API key would avoid it is correct: auth_mode: "apikey" (or BB_TRANSCRIPTION=openai/… + OPENAI_API_KEY) goes to api.openai.com/v1/audio/transcriptions, which is not behind that WAF rule. Note that the issue body does not quote an error string (it only says "fails with HTTP 403 due to cloudflare"); the exact wording shown below is what the code produces on a challenged network, reproduced here by simulation.
Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Transcription fails with HTTP 403 from Cloudflare when using a Codex subscription | Verified (mechanism) | https://chatgpt.com/backend-api/transcribe is behind Cloudflare managed challenge (HTTP 403, cf-mitigated: challenge, HTML body; captured in cf-challenge-headers.txt). The daemon's transcription code path is the only bb code that hits it, and only for auth_mode: "chatgpt". End-to-end repro below shows exactly the reported error text. |
| "seems to usually fail on my machine" | Unverifiable here; plausible | Cloudflare's decision is per-network and fluctuates: with the daemon's headers I got 0/18 challenges (8 real daemon calls + 10 curl); with Authorization only, 7/10; unauthenticated, 10/10 with every UA. A network with a worse bot score sees the same fuzziness with the full headers. See waf-sample.log. |
| "it hits a chatgpt.com endpoint meant for internal use vs an actual official api endpoint" | Verified | CHATGPT_TRANSCRIBE_URL = "https://chatgpt.com/backend-api/transcribe" (codex-chatgpt-client.ts#L28). The installed codex-cli 0.147.0 binary contains no reference to /backend-api/transcribe (its voice input uses realtime/calls); the string only exists in the ChatGPT web client. Probing /backend-api/codex/transcribe and two other codex-prefixed variants returns 404 (log), so there is no un-challenged Codex-scoped equivalent. |
| "I suspect an API key would fix it" | Verified by code path (not run: no API key here) | fetchTranscription routes auth.type === "apiKey" to https://api.openai.com/v1/audio/transcriptions (L905-L934); the server-side openai/* route does the same. Neither goes through chatgpt.com. |
| Comment "same here" (jandornig) | No details | Second independent report; no version, OS or network info. |
Environment
- bb worktree
/home/sawyer/projects/bb/.claude/worktrees/wf_242c3e11-a10-20. The worktree HEAD wasa108fa7ef(5 commits after the base16ceb3a54);git diff 16ceb3a54 HEADis empty for every file involved (codex-chatgpt-client.ts,chatgpt-cloudflare-cookies.ts,voice-transcription.ts,inference.ts), and permalinks below point at the base commit.git log 16ceb3a54..origin/maintouches none of them either: not fixed on main. - Linux 7.0.0-29-generic (Ubuntu), node v24.18.0, pnpm 9.15.0, codex-cli 0.147.0 with
~/.codex/auth.jsonauth_mode: "chatgpt"(subscription). DefaultBB_TRANSCRIPTION=codex/gpt-transcribe. - Dev instance (original run): app
:16862, server:24862, host daemon:32862, data dir/home/sawyer/.bb-dev/projects-bb-.claude-worktrees-wf_242c3e11-a10-20-905936e6568b. Revision re-run (step D output below): worktreewf_6b6686dc-4c2-26(same HEADa108fa7ef), server:25796, host daemon:33796, data dir/home/sawyer/.bb-dev/projects-bb-.claude-worktrees-wf_6b6686dc-4c2-26-1b40c9cc9b2f. Both started withNODE_OPTIONS="--import /tmp/bb-reports/issues/1587/repro/cf-challenge-preload.mjs"so the daemon can be switched between the real network and a simulated Cloudflare challenge without a restart (see repro). - Test audio: tone.mp3 (1 s 440 Hz sine, generated with ffmpeg; the real endpoint transcribes it as
"...").
Minimal reproduction
Three layers, from "prove the endpoint is behind a Cloudflare challenge" to "the CLI error the code produces on a challenged network" (the issue itself does not quote the error text). No secrets are printed by any script; the auth probes read ~/.codex/auth.json in-process.
A. The endpoint is behind Cloudflare managed challenge; the Codex endpoint is not (live, no bb involved)
Script: waf-probe.sh. Unauthenticated POSTs to both URLs with five User-Agents. A request that passes the WAF (Cloudflare's web application firewall) reaches the app and gets 401; a challenged one gets 403 + cf-mitigated: challenge and never reaches the app.
$ bash /tmp/bb-reports/issues/1587/repro/waf-probe.sh UA=bb-host-daemon https://chatgpt.com/backend-api/transcribe HTTP/2 403 cf-mitigated: challenge server: cloudflare UA=bb-host-daemon https://chatgpt.com/backend-api/codex/responses HTTP/2 401 server: cloudflare UA=codex_cli_rs/0.147.0 (Linux …) https://chatgpt.com/backend-api/transcribe HTTP/2 403 cf-mitigated: challenge server: cloudflare UA=codex_cli_rs/0.147.0 (Linux …) https://chatgpt.com/backend-api/codex/responses HTTP/2 401 server: cloudflare UA=curl/8.5.0 https://chatgpt.com/backend-api/transcribe HTTP/2 403 cf-mitigated: challenge server: cloudflare UA=curl/8.5.0 https://chatgpt.com/backend-api/codex/responses HTTP/2 401 server: cloudflare UA=python-requests/2.31 https://chatgpt.com/backend-api/transcribe HTTP/2 403 cf-mitigated: challenge server: cloudflare UA=python-requests/2.31 https://chatgpt.com/backend-api/codex/responses HTTP/2 401 server: cloudflare UA=Mozilla/5.0 (… Chrome/128.0 …) https://chatgpt.com/backend-api/transcribe HTTP/2 403 cf-mitigated: challenge server: cloudflare UA=Mozilla/5.0 (… Chrome/128.0 …) https://chatgpt.com/backend-api/codex/responses HTTP/2 401 server: cloudflare
Ran twice, 10 min apart, identical result (run 1, run 2). The full challenge response is saved: headers, 10,492-byte HTML body ("Just a moment…", loads /cdn-cgi/challenge-platform/…).
B. Which headers matter, and how "often" it fails (live)
Scripts: waf-probe-headers.py (one request per header combination) and waf-sample.py (10 interleaved pairs). Both send the daemon's multipart body and User-Agent.
$ python3 /tmp/bb-reports/issues/1587/repro/waf-probe-headers.py
auth+account+originator HTTP/2 200 body={"text":"...","asset_pointer":"sediment://file_…
auth+account HTTP/2 200 body={"text":"...", …
auth+originator=bb HTTP/2 200 body={"text":"...", …
auth+originator=codex_cli_rs HTTP/2 200 body={"text":"...", …
auth only HTTP/2 403 cf-mitigated: challenge body=<html> <head> <meta name="viewport" …
account only (no auth) HTTP/2 401 body={"detail":"Unauthorized"}
originator only (no auth) HTTP/2 401 body={"detail":"Unauthorized"}
nothing HTTP/2 403 cf-mitigated: challenge body=<html> <head> …
$ python3 /tmp/bb-reports/issues/1587/repro/waf-sample.py | tail -1
tally: {"full-daemon-headers": {"200": 10}, "authorization-only": {"403": 7, "200": 3}}
Read: on this network the presence of chatgpt-account-id or originator (which the daemon does send) is enough to pass, and without them the outcome is a coin flip weighted toward the challenge (7/10). That coin flip is what "often fails" looks like; the reporters' networks are scored so that it applies even with the full header set. Re-sending the challenge's own cookies is what the daemon's single retry does; with curl and a cookie jar (waf-cookie-retry.sh) my first run went 403, 403, 401 and a second run a minute later 401 ×8 (log of the second run): the retry can help, but only sometimes, and never deterministically.
C. The daemon path against the real endpoint (live, this network): passes
live-probe-1587.ts calls the real transcribeCodexVoice() from apps/host-daemon/src/codex-chatgpt-client.ts with the local subscription auth. Copy it into apps/host-daemon/ and run pnpm exec tsx live-probe-1587.ts /tmp/bb-reports/issues/1587/repro/tone.mp3 8:
#1 OK 580ms text="..." #2 OK 896ms text="..." #3 OK 521ms text="..." #4 OK 524ms text="..." #5 OK 587ms text="..." #6 OK 500ms text="..." #7 OK 655ms text="..." #8 OK 1602ms text="..."
So the live failure is network-dependent and did not occur from here with the daemon's headers. Steps D and E reproduce what happens on a network where it does.
D. End to end: bb voice transcribe on a challenged network (deterministic simulation)
cf-challenge-preload.mjs is a Node --import preload that wraps globalThis.fetch: while the flag file FORCE_CF_CHALLENGE exists, any POST to https://chatgpt.com/backend-api/transcribe gets the real captured Cloudflare response from step A (403, cf-mitigated: challenge, the HTML body, a __cf_bm set-cookie); everything else goes to the network. It logs each intercepted call, which lets us count the daemon's attempts. Both scripts are configured through the environment: the preload reads CF_PRELOAD_DIR (flag file + log location, default /tmp/bb-1587-preload) and finds cf-challenge-body.html next to itself; the wrapper run-cli-transcribe.sh reads WORKTREE (default $PWD), BB_SERVER_URL (required) and the same CF_PRELOAD_DIR. Nothing needs editing.
- In your built bb worktree, start your dev instance with the preload:
export NODE_OPTIONS="--import /tmp/bb-reports/issues/1587/repro/cf-challenge-preload.mjs" && scripts/bb-dev-app current(the launcher passes the environment to the server and daemon). Thenunset NODE_OPTIONS; eval "$(scripts/bb-dev-app env)"; export WORKTREE=$PWDand confirmcurl -s $BB_SERVER_URL/api/v1/system/configshows"voiceTranscriptionEnabled":true(give it ~30 s to boot). - Baseline (real network):
bash /tmp/bb-reports/issues/1587/repro/run-cli-transcribe.sh baseline. Expected and actual: the CLI prints...(the transcript of the 1 s tone, printed as plain text because the wrapper does not pass--json) andexit=0: - Challenged:
bash /tmp/bb-reports/issues/1587/repro/run-cli-transcribe.sh challenge.
$ bash /tmp/bb-reports/issues/1587/repro/run-cli-transcribe.sh baseline
$ bb voice transcribe tone.mp3 --type audio/mpeg # mode=baseline server=http://localhost:25796
...
exit=0
$ bash /tmp/bb-reports/issues/1587/repro/run-cli-transcribe.sh challenge
$ bb voice transcribe tone.mp3 --type audio/mpeg # mode=challenge server=http://localhost:25796
Error: HTTP 502: Codex transcription request failed with HTTP 403: <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style global>body{font-family:Arial,Helvetica,sans-serif}.container{align-items:center;display:flex;flex-direction:column;gap:2rem;height:100%;justify-content:center;width:100%}@keyframes enlarge-appear{0%{opacity:0;transform:scale(75%) rotate(-90deg)}to{opacity:1;transform:scale(100%) rotate(0deg)}}.lo...
exit=1
--- daemon fetch calls intercepted by the preload during this run (/tmp/bb-1587-preload/cf-challenge-preload.log):
2026-08-18T14:27:13.470Z pid=3478951 intercepted POST https://chatgpt.com/backend-api/transcribe cookie="__cf_bm=6DH8…; __cflb=04dT…; _cfuvid=zv0y…" ua=bb-host-daemon
2026-08-18T14:27:13.471Z pid=3478951 intercepted POST https://chatgpt.com/backend-api/transcribe cookie="__cf_bm=SIMULATED.CHALLENGE.COOKIE-1787038601; __cflb=04dT…; _cfuvid=zv0y…" ua=bb-host-daemon
Expected: a transcript, or at least a retry/fallback and a human-readable error. Actual: exit 1 with the error the code produces for a challenged request (HTTP 502 wrapping "HTTP 403" and the challenge page's CSS). The preload log shows the daemon's two fetches (original + one cookie retry, the second carrying the __cf_bm from the challenge). The server log for the same second shows a single attempt out of two allowed and the daemon's error code:
@bb/host-daemon:dev: [14:27:13] DEBUG: [host-daemon] Online host RPC {"serverUrl":"http://127.0.0.1:25796","commandType":"codex.voice.transcribe","errorCode":"codex_request_failed","handlerMs":1.9,"ok":false}
@bb/server:dev: [14:27:13] WARN: [server] Voice transcription failed {"attempts":1,"durationMs":7,"maxAttempts":2,"model":"codex/gpt-transcribe","reason":"failed"}
Full CLI output: cli-challenge-run.log. Running run-cli-transcribe.sh baseline again (which removes the flag file /tmp/bb-1587-preload/FORCE_CF_CHALLENGE) makes the same instance transcribe again (..., exit=0). Preload log of the run: cf-challenge-preload.log.
E. Unit-level repro test (fails on main)
codex-chatgpt-client-1587.test.ts: drop it at apps/host-daemon/src/codex-chatgpt-client-1587.test.ts and run cd apps/host-daemon && pnpm exec vitest run src/codex-chatgpt-client-1587.test.ts. It stubs fetch with the challenge response (real HTML head) and calls the real transcribeCodexVoice() against a temp CODEX_HOME with a ChatGPT-mode auth.json. Test 1 documents today's behavior and passes; test 2 asserts the behavior the server needs and fails:
✓ documents current behavior: one cookie retry, then a permanent codex_request_failed carrying raw HTML
× EXPECTED (fails on main): a Cloudflare challenge is transient and must not leak the challenge HTML
→ expected 'codex_request_failed' to be 'codex_service_unavailable' // Object.is equality
❯ src/codex-chatgpt-client-1587.test.ts:130:26
Test Files 1 failed (1) Tests 1 failed | 1 passed (2)
Which assertion fails and why: expect(failure.code).toBe("codex_service_unavailable") at line 130 receives codex_request_failed because codexRequestErrorCode(403) falls through to the generic code; the following assertion (message must not contain <html>) would fail too. Full log: vitest-1587.log. The test file inline:
// Repro for get-bb/bb#1587 (excerpt; full file linked above)
function cloudflareChallengeResponse(): Response {
return new Response(CF_CHALLENGE_HTML, {
status: 403,
headers: {
"content-type": "text/html; charset=UTF-8",
"cf-mitigated": "challenge",
server: "cloudflare",
"set-cookie": "__cf_bm=cloudflare-cookie; HttpOnly; SameSite=None; Secure; Path=/; Domain=chatgpt.com",
},
});
}
it("documents current behavior: one cookie retry, then a permanent codex_request_failed carrying raw HTML", async () => {
await writeChatGptAuth();
const fetchMock = vi.fn<typeof fetch>();
fetchMock.mockImplementation(async () => cloudflareChallengeResponse());
vi.stubGlobal("fetch", fetchMock);
const failure = await runTranscribe();
expect(fetchMock).toHaveBeenCalledTimes(2);
const retryHeaders = new Headers(fetchMock.mock.calls[1]?.[1]?.headers);
expect(retryHeaders.get("cookie")).toBe("__cf_bm=cloudflare-cookie");
expect(failure.code).toBe("codex_request_failed");
expect(failure.message).toContain(
"Codex transcription request failed with HTTP 403: <html> <head> <meta name=\"viewport\"",
);
});
it("EXPECTED (fails on main): a Cloudflare challenge is transient and must not leak the challenge HTML", async () => {
await writeChatGptAuth();
const fetchMock = vi.fn<typeof fetch>();
fetchMock.mockImplementation(async () => cloudflareChallengeResponse());
vi.stubGlobal("fetch", fetchMock);
const failure = await runTranscribe();
expect(failure.code).toBe("codex_service_unavailable"); // <-- FAILS on main: "codex_request_failed"
expect(failure.message).not.toContain("<html>"); // <-- FAILS on main
});
Root cause
1. The subscription transcription path uses a ChatGPT web-app endpoint that is behind Cloudflare bot management. codex-chatgpt-client.ts#L27-L30:
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses"; const CHATGPT_TRANSCRIBE_URL = "https://chatgpt.com/backend-api/transcribe"; const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses"; const OPENAI_TRANSCRIBE_URL = "https://api.openai.com/v1/audio/transcriptions";
fetchTranscription (L920-L934) picks CHATGPT_TRANSCRIBE_URL whenever readCodexAuthCredentials() returns type: "chatgpt". Step A shows that Cloudflare applies a managed challenge to /backend-api/transcribe and not to /backend-api/codex/responses; the challenge requires executing JavaScript from /cdn-cgi/challenge-platform/, which a Node fetch client cannot do. Codex CLI itself never calls this URL (its binary has no such string), so OpenAI has no reason to exempt non-browser clients on it. The headers bb sends (createChatGptHeaders: bearer, chatgpt-account-id, originator: bb, User-Agent: bb-host-daemon) lower the challenge rate here to 0/18 but the decision is made by Cloudflare per client and per network; the reporters' clients get challenged "usually".
2. The daemon's mitigation is a single blind retry. isCloudflareChallenge detects the header, and fetchChatGpt re-sends once with whatever Cloudflare service cookies were stored (chatgpt-cloudflare-cookies.ts, in-memory, process-global):
const response = await fetchOnce();
if (!isCloudflareChallenge(response)) {
return response;
}
return fetchOnce();
A challenge response only sets __cf_bm; cf_clearance (the cookie that actually clears a challenge) is issued only after the JS challenge is solved. So the retry works only if Cloudflare's score happens to flip (step B: 403, 403, 401), and the second 403 is then treated like any other HTTP error.
3. The 403 is classified as a permanent failure, so the server neither retries nor falls back, and the raw HTML is put in the message. codexRequestErrorCode maps 401, 429 and 5xx; 403 falls to codex_request_failed. createCodexHttpError then appends extractProviderErrorMessage(readErrorText(...)), which for a text/html body is the whitespace-collapsed HTML truncated to 400 chars. On the server, isTransientInferenceError only recognises codex_rate_limited, codex_service_unavailable, codex_request_timeout, command_timeout; inferenceCompleteWithFallback therefore stops after attempt 1 of maxAttempts: 2 (server log: "attempts":1,"maxAttempts":2), and transcribeWithCodexHostDaemon re-throws the daemon error as-is: HTTP 502: Codex transcription request failed with HTTP 403: <html> …. Note also that the transcription "fallback" is the same model (fallbackModel: transcriptionModel), so even a transient classification would only buy one more try of the same challenged endpoint after 250 ms, not a switch to api.openai.com.
Deeper issue. bb depends on an undocumented ChatGPT web endpoint whose bot protection is outside its control; a Cloudflare rule change can break subscription-mode voice for everyone at once. There is no Codex-scoped transcription endpoint to move to (step A/codex-path probe = 404). The realistic goals are: don't make a challenge look like a bb bug, retry/fallback sensibly, and tell the user what to configure.
Proposed fix (first principles)
- Daemon (host-local primitive: classify the provider response). In
transcribeCodexVoice/createCodexHttpError, whenisCloudflareChallenge(response)is still true after the retry, throw a dedicatedExpectedCommandDispatchError— eithercodex_service_unavailablewith a clean message such as "ChatGPT transcription endpoint returned a Cloudflare challenge (HTTP 403); this network is being bot-filtered. Try again, or configure OPENAI_API_KEY / an API-key Codex login for transcription." — or a new code (codex_cloudflare_challenge) that the server adds toisTransientInferenceError. Also stop leaking HTML in general: increateCodexHttpError, if the responsecontent-typeistext/html, do not append the body. Test 2 of the repro file is the regression test. Since only the error code/message content changes and both are already free-form strings on the wire, noHOST_DAEMON_PROTOCOL_VERSIONbump is strictly needed for the first variant; a new code that the server switches on is a wire-meaning change and should bump it. - Server (product policy). In
transcribeWithCodexHostDaemon, make the fallback meaningful: ifdeps.config.openAiApiKeyis set, useopenai/<same model id>(the existingtranscribeWithOpenAipath againstapi.openai.com) as the fallback attempt after a transient/challenge failure instead offallbackModel: transcriptionModel; otherwise keep the one delayed retry (a real retry does sometimes pass, step B). Map the final challenge failure to the existingbuildTranscriptionUnavailableError()(503transcription_unavailable) so the CLI/app show a sentence, not CSS. Document indocs/configuration.mdthat subscription-mode voice can be blocked by Cloudflare and thatBB_TRANSCRIPTION=openai/gpt-4o-transcribe+OPENAI_API_KEYavoids chatgpt.com. - Optional hardening (daemon). Persist the Cloudflare cookies across daemon restarts (they are already scoped and allow-listed) and consider mirroring what a browser sends more closely (e.g.
Accept,Origin: https://chatgpt.com,Referer), measured against real challenge rates. I did not verify that this changes Cloudflare's score; step B suggestsoriginator/chatgpt-account-idalready carry most of the weight here.
What could go wrong: classifying every 403 as transient would hide real permission errors; key the change on cf-mitigated: challenge only. Falling back to api.openai.com spends API credit for users who thought they were on a subscription; gate it on an explicit configured key and log it. What would settle the "why their network" question: a reporter running waf-sample.py from their machine (it prints only status codes) and pasting the tally.
Related issues
- #1330 "Fix transient thread title inference failures" — introduced the transient/fallback policy (
isTransientInferenceError,inferenceCompleteWithFallback) that this fix should plug into. - Commit
adea82e1d(2026-06-08) "retry Codex voice transcription on transient failures" andf17e8e4b6(2026-05-18) which added the Cloudflare cookie retry — the existing, insufficient mitigations. - No other open issue or PR mentions transcription/Cloudflare (searched
transcri OR cloudflare OR voice).
Appendix
Files
- 1587/repro/:
tone.mp3,waf-probe.sh,waf-probe-auth.sh,waf-probe-headers.py,waf-sample.py,waf-cookie-retry.sh,waf-probe-codex-path.py,cf-challenge-headers.txt,cf-challenge-body.html,cf-challenge-preload.mjs,run-cli-transcribe.sh,live-probe-1587.ts,codex-chatgpt-client-1587.test.ts. - Logs: waf-probe.log, waf-probe-2.log, waf-probe-auth.log, waf-probe-headers.log, waf-sample.log, waf-cookie-retry.log, waf-probe-codex-path.log, cli-challenge-run.log, vitest-1587.log, codex-strings.txt (strings of the codex 0.147.0 binary, used to check it never calls
/backend-api/transcribe).
Captured challenge response headers (step A)
HTTP/2 403 date: Tue, 18 Aug 2026 07:36:41 GMT content-type: text/html; charset=UTF-8 content-length: 10492 cf-mitigated: challenge server: cloudflare server-timing: chlray;desc="a2cf3fb8fc1c64b6" set-cookie: __cf_bm=…; HttpOnly; SameSite=None; Secure; Path=/; Domain=chatgpt.com; Expires=Tue, 18 Aug 2026 08:06:41 GMT cf-ray: a2cf3fb8fc1c64b6-SJC
Codex-prefixed transcription paths do not exist
$ python3 /tmp/bb-reports/issues/1587/repro/waf-probe-codex-path.py
https://chatgpt.com/backend-api/codex/transcribe HTTP/2 404 body={"detail":"Not Found"}
https://chatgpt.com/backend-api/codex/audio/transcriptions HTTP/2 404 body={"detail":"Not Found"}
https://chatgpt.com/backend-api/codex/v1/audio/transcriptions HTTP/2 404 body={"detail":"Not Found"}
https://chatgpt.com/backend-api/transcribe HTTP/2 200 body={"text":"...","asset_pointer":"sediment://file_…
Codex CLI binary does not reference the endpoint
$ strings -n 6 ~/.codex/packages/standalone/current/bin/codex > codex-strings.txt
$ grep -oE ".{0,60}(backend-api/transcribe|/transcribe|audio/transcriptions).{0,60}" codex-strings.txt | sort -u
(no output)
$ grep -oE ".{0,40}realtime/calls.{0,40}" codex-strings.txt | head -1
…sdpsession/backend-apirealtime/callscodex-realtime-call-boundaryContent-Dispositio…
Commands run (in order)
gh issue view 1587 --comments pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build git fetch origin main; git log 16ceb3a54..origin/main --oneline -- apps/host-daemon/src/codex-chatgpt-client.ts apps/host-daemon/src/chatgpt-cloudflare-cookies.ts apps/host-daemon/src/codex-auth.ts apps/server/src/services/ai codex --version; strings -n 6 ~/.codex/packages/standalone/current/bin/codex > /tmp/bb-reports/issues/1587/codex-strings.txt ffmpeg -f lavfi -i "sine=frequency=440:duration=1" -ac 1 -ar 16000 -c:a libmp3lame -q:a 6 tone.mp3 cd apps/host-daemon && pnpm exec tsx live-probe-1587.ts /tmp/bb-reports/issues/1587/repro/tone.mp3 8 bash repro/waf-probe.sh; bash repro/waf-probe-auth.sh; python3 repro/waf-probe-headers.py; bash repro/waf-cookie-retry.sh (x2); python3 repro/waf-probe-codex-path.py; python3 repro/waf-sample.py curl -s -o repro/cf-challenge-body.html -D repro/cf-challenge-headers.txt -X POST -A bb-host-daemon https://chatgpt.com/backend-api/transcribe -F file=@repro/tone.mp3 -F model=gpt-transcribe export NODE_OPTIONS="--import /tmp/bb-reports/issues/1587/repro/cf-challenge-preload.mjs" && scripts/bb-dev-app current unset NODE_OPTIONS; eval "$(scripts/bb-dev-app env)"; export WORKTREE=$PWD curl -s $BB_SERVER_URL/api/v1/system/config # voiceTranscriptionEnabled: true bash /tmp/bb-reports/issues/1587/repro/run-cli-transcribe.sh baseline # "..." exit=0 bash /tmp/bb-reports/issues/1587/repro/run-cli-transcribe.sh challenge # HTTP 502 ... HTTP 403 ... exit=1 bash /tmp/bb-reports/issues/1587/repro/run-cli-transcribe.sh baseline # "..." exit=0 again grep -a "Voice transcription failed\|codex.voice.transcribe" ~/.bb-dev/launchers/projects-bb-.claude-worktrees-wf_6b6686dc-4c2-26/dev.log cd apps/host-daemon && pnpm exec vitest run src/codex-chatgpt-client-1587.test.ts pnpm dev:stop
Verification
An independent verifier followed all five layers (A–E) in a separate worktree at the same HEAD and confirmed every result: waf-probe.sh gave the identical 403/cf-mitigated: challenge vs 401 table; waf-probe-headers.py gave the identical header-combination table and waf-sample.py tallied full-daemon-headers 10/10 200 vs authorization-only 9/10 challenged (7/10 in the original run: the "coin flip"); live-probe-1587.ts 8/8 OK; the dev-instance run reproduced the exact HTTP 502: Codex transcription request failed with HTTP 403: <html>… output with two intercepted daemon fetches, codex_request_failed in the daemon log and attempts:1,maxAttempts:2 in the server log; vitest 1 passed / 1 failed at the codex_service_unavailable assertion. All root-cause code excerpts were confirmed at 16ceb3a54, and git log 16ceb3a54..origin/main touches none of the files.
Changed in this revision. (1) The verifier's one major finding: run-cli-transcribe.sh and cf-challenge-preload.mjs were hardcoded to the original worktree path, ports and flag/log paths (and the wrapper deleted the preload-log artifact on every run). Both were rewritten to take WORKTREE, BB_SERVER_URL and CF_PRELOAD_DIR from the environment (defaults: $PWD, required, /tmp/bb-1587-preload), the preload locates cf-challenge-body.html relative to itself, and the wrapper only prints the log lines appended during its own run. Step D was then re-run from scratch in a third worktree (wf_6b6686dc-4c2-26, ports 25796/33796) with the new scripts and no manual edits; the baseline / challenge / baseline outputs and log lines shown in step D and cli-challenge-run.log are from that re-run. (2) Minor: step D's baseline expectation now says the CLI prints ... (no --json). (3) Minor: "the reporter's error" is now phrased as the error the code produces on a challenged network, with a note that the issue does not quote an error string. (4) Minor: WAF is expanded once, and the per-network bot-score explanation is marked as inferred in the TL;DR.