You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Codex Desktop MCP auto connection authorization bug #23866
What version of the Codex App are you using (From “About Codex” dialog)?
26.513.31313 (2867)
What subscription do you have?
API
What platform is your computer?
Darwin 25.4.0 arm64 arm
What issue are you seeing?
When I am connecting to MCP servers (in my case Notion and Linear), either:
I use codex mcp login notion , codex mcp login linear from command line, it opens a browser page with the url it gives asking to trust the 127.0.0.1, i approve, it shows approved in the browser, then I force quit the codex app, and reopen the runtime, it is connected (This one works)
If I directly tell Codex to look at a ticket in Linear (I had the MCP authenticated before but it does not stays authenticated forever), it launches automatically the webpage for authentication, I click, web page says it is authenticated, but The XXX MCP is configured, but its refresh token is expired/invalid. I’m going to re-authenticate it now; the skill notes a restart may be needed after OAuth, but I’ll see if this live session can pick it up. and then does not continue with this MCP
What steps can reproduce the bug?
Go into a session with the coding agent, when Linear/Notion is not yet authenticated/authenticated before but not now;
Ask Codex to see Linear ticket xxx, It will pop the authenticate web page, you click authenticate 127.0.0.1, and it will say authenticated, but then Codex will say: The XXX MCP is configured, but its refresh token is expired/invalid. I’m going to re-authenticate it now; the skill notes a restart may be needed after OAuth, but I’ll see if this live session can pick it up.
What is the expected behavior?
After I approve the authentication and the page says authenticated, Codex can access the MCP
openai/codex #23866 — Codex Desktop MCP auto connection authorization bug
Severity: High (auth, mcp, app)
Platform: macOS arm64
Version: 26.513.31313 (2867)
Comments: 0 (zero competition!)
Root Cause Analysis
The bug manifests as a token lifecycle management failure in the Desktop app's OAuth flow:
CLI auth works: codex mcp login notion → browser auth → token saved to disk → app restart picks up token → connected
Auto-auth fails: Session triggers auth → browser auth succeeds → token NOT persisted to the right location → app reports "refresh token expired/invalid" → gives up
The root cause is a race condition / state synchronization gap between:
The browser-based OAuth callback (which writes the token to the CLI token store)
The Desktop app's in-memory token cache (which doesn't get notified of the update)
The Desktop app's persistent token store (which may use a different path than the CLI)
When the Desktop app initiates auth from a live session, the browser callback writes the token to the CLI's token location, but the Desktop app:
Doesn't watch for changes to that file
May use a different token storage path
Doesn't reload its token cache after the browser callback completes
Solution A (Recommended): Optimistic Token Refresh with Polling
After the browser auth flow completes, poll for token availability and retry the MCP call:
// In the Desktop app's MCP auth handlerclassMCPAuthManager{privatetokenStore: TokenStore;privatetokenWatcher: FileWatcher|null=null;asyncauthenticateMCP(connectorName: string): Promise<MCPConnection>{// 1. Get the auth URL and open browserconstauthUrl=awaitthis.getAuthUrl(connectorName);awaitopenBrowser(authUrl);// 2. Start watching for token file changesconsttokenPath=this.getTokenPath(connectorName);consttokenChanged=this.waitForTokenChange(tokenPath,{timeout: 120_000});// 3. Wait for either token change or timeoutconstresult=awaitPromise.race([tokenChanged.then(()=>'token_ready'),sleep(120_000).then(()=>'timeout')]);if(result==='timeout'){thrownewAuthError(`Authentication timed out for ${connectorName}`);}// 4. Reload token from diskconsttoken=awaitthis.tokenStore.loadToken(connectorName);if(!token||this.isTokenExpired(token)){// Token file exists but is invalid — try one more reloadawaitsleep(2000);constrefreshedToken=awaitthis.tokenStore.loadToken(connectorName);if(!refreshedToken||this.isTokenExpired(refreshedToken)){thrownewAuthError(`Token loaded but expired for ${connectorName}. `+`Please try \`codex mcp login ${connectorName}\` from CLI.`);}// Use the refreshed tokenreturnthis.connectWithToken(connectorName,refreshedToken);}returnthis.connectWithToken(connectorName,token);}privateasyncwaitForTokenChange(tokenPath: string,options: {timeout: number}): Promise<void>{returnnewPromise((resolve,reject)=>{consttimeout=setTimeout(()=>reject(newError('Timeout')),options.timeout);constwatcher=chokidar.watch(tokenPath,{ignoreInitial: true});watcher.on('change',()=>{clearTimeout(timeout);watcher.close();resolve();});watcher.on('add',()=>{clearTimeout(timeout);watcher.close();resolve();});});}}
Solution B: Unified Token Storage Path
Ensure CLI and Desktop app use the same token storage location:
// Token store configurationconstTOKEN_STORE_CONFIG={// CLI uses: ~/.codex/tokens/// Desktop should use the same:tokenDir: path.join(os.homedir(),'.codex','tokens'),// Or use OS keychain for cross-process sharing:useKeychain: process.platform==='darwin',keychainService: 'com.openai.codex',};// Unified token storeclassUnifiedTokenStore{asyncsaveToken(connectorName: string,token: OAuthToken): Promise<void>{if(TOKEN_STORE_CONFIG.useKeychain){awaitkeytar.setPassword(TOKEN_STORE_CONFIG.keychainService,`mcp-${connectorName}`,JSON.stringify(token));}else{consttokenPath=path.join(TOKEN_STORE_CONFIG.tokenDir,`${connectorName}.json`);awaitfs.mkdir(path.dirname(tokenPath),{recursive: true});awaitfs.writeFile(tokenPath,JSON.stringify(token,null,2));}}asyncloadToken(connectorName: string): Promise<OAuthToken|null>{if(TOKEN_STORE_CONFIG.useKeychain){constraw=awaitkeytar.getPassword(TOKEN_STORE_CONFIG.keychainService,`mcp-${connectorName}`);returnraw ? JSON.parse(raw) : null;}else{consttokenPath=path.join(TOKEN_STORE_CONFIG.tokenDir,`${connectorName}.json`);try{constraw=awaitfs.readFile(tokenPath,'utf-8');returnJSON.parse(raw);}catch{returnnull;}}}}
Solution C: Desktop App Token Cache Invalidation
Add a file watcher to the Desktop app that invalidates the token cache when the CLI updates the token file:
// Desktop app token cache with file watchingclassDesktopTokenCache{privatecache: Map<string,OAuthToken>=newMap();privatewatchers: Map<string,FSWatcher>=newMap();startWatching(connectorName: string): void{consttokenPath=this.getTokenPath(connectorName);if(this.watchers.has(tokenPath))return;// Already watchingconstwatcher=chokidar.watch(tokenPath,{ignoreInitial: true});watcher.on('change',async()=>{// Token file changed — reload from diskconsttoken=awaitthis.loadToken(connectorName);if(token){this.cache.set(connectorName,token);logger.info(`Token refreshed for ${connectorName}`);// Notify active MCP connections to reconnectthis.emit('tokenRefreshed',{ connectorName, token });}});this.watchers.set(tokenPath,watcher);}getToken(connectorName: string): OAuthToken|null{// Always check disk first, then cachereturnthis.cache.get(connectorName)||null;}}
Testing Strategy
Reproduction test: Follow the exact steps in the issue — verify the fix resolves the auto-auth failure
CLI vs Desktop test: Verify both auth methods produce the same token state
Token expiry test: Verify token refresh works when refresh token expires
Multi-connector test: Verify auth for Notion + Linear simultaneously
Impact Assessment
Metric
Value
Severity
High (MCP connectors unusable via Desktop auto-auth)
Implement Solution A as the immediate fix (optimistic polling) and Solution B as the long-term fix (unified token storage). Both can be implemented in parallel. This is a zero-competition issue with a clear reproduction path.
A useful way to make this debuggable would be to expose the auth handoff as explicit states, not only as a final “expired/invalid” error.
For this flow there are at least four distinct points that can fail:
Desktop starts OAuth and records the connector/session that requested it.
Browser callback succeeds and writes/updates a token somewhere.
Desktop observes the updated token store or keychain entry.
The live MCP connection is rebuilt with that token and re-runs tool discovery.
Right now the user sees the browser success page, then the agent still reports an invalid refresh token, so the observable state jumps from step 2 to failure without telling whether step 3 or step 4 broke.
A small diagnostic would help a lot here: after callback, log the token store/keychain location used by Desktop, whether a fresh token was observed, whether the token was accepted by the remote MCP server, and whether tools/list was retried. No raw token values needed. That would separate “token persisted to the wrong place” from “Desktop cache did not reload” from “connection was not rebuilt after reload.”
Activity
jshaofa-ui commentedon May 21, 2026
Solution: Codex Desktop MCP Auto-Connection Authorization Failure
Issue
openai/codex #23866 — Codex Desktop MCP auto connection authorization bug
Root Cause Analysis
The bug manifests as a token lifecycle management failure in the Desktop app's OAuth flow:
codex mcp login notion→ browser auth → token saved to disk → app restart picks up token → connectedThe root cause is a race condition / state synchronization gap between:
When the Desktop app initiates auth from a live session, the browser callback writes the token to the CLI's token location, but the Desktop app:
Solution A (Recommended): Optimistic Token Refresh with Polling
After the browser auth flow completes, poll for token availability and retry the MCP call:
Solution B: Unified Token Storage Path
Ensure CLI and Desktop app use the same token storage location:
Solution C: Desktop App Token Cache Invalidation
Add a file watcher to the Desktop app that invalidates the token cache when the CLI updates the token file:
Testing Strategy
Impact Assessment
Estimated Effort
Recommendation
Implement Solution A as the immediate fix (optimistic polling) and Solution B as the long-term fix (unified token storage). Both can be implemented in parallel. This is a zero-competition issue with a clear reproduction path.
norika1207-lab commentedon Jun 1, 2026
A useful way to make this debuggable would be to expose the auth handoff as explicit states, not only as a final “expired/invalid” error.
For this flow there are at least four distinct points that can fail:
Right now the user sees the browser success page, then the agent still reports an invalid refresh token, so the observable state jumps from step 2 to failure without telling whether step 3 or step 4 broke.
A small diagnostic would help a lot here: after callback, log the token store/keychain location used by Desktop, whether a fresh token was observed, whether the token was accepted by the remote MCP server, and whether
tools/listwas retried. No raw token values needed. That would separate “token persisted to the wrong place” from “Desktop cache did not reload” from “connection was not rebuilt after reload.”liangxiwei commentedon Sep 1, 2026
it's a stupid bug.