Skip to content

[Bug] setup --json reports verified:true but rescue agent fails with 'refresh token already used' (false-positive advisory) #2

Closed

Description

@RoastFried-RF

[Bug] setup --json reports loggedIn:true, verified:true but rescue/agent invocation fails with refresh token already used

Summary

codex-companion.mjs setup --json advisory consistently reports the auth as
healthy (auth.loggedIn:true, auth.verified:true, ready:true), but the very
next rescue-agent invocation (Agent({subagent_type:"opnd-codex:codex-rescue"}))
fails with Your access token could not be refreshed because your refresh token was already used. and never reaches Codex.

Re-running codex logout && codex login (or re-authenticating in Codex
Desktop) makes the next setup --json succeed again, but the very next
rescue-agent invocation still fails with the same refresh token already used
error. Observed 3 consecutive cycles of (re-auth → setup verified → rescue
fail) in a single session.

This makes the setup verification effectively a false-positive advisory:
the host sees green light, retries the work, and burns a Codex session every
time.

Environment

Field Value
Plugin opnd-io/opnd-codex-plugin (this repo)
Plugin version 2.1.0 (or latest Unreleased per CHANGELOG, observed locally at plugins/opnd-codex/ HEAD)
Codex CLI codex-cli 0.130.0; advanced runtime available
Auth source app-server (ChatGPT, authMethod: chatgpt)
Account ChatGPT subscription
Host Windows 11 Pro 10.0.26200 (PowerShell + Git Bash)
Claude Code Opus 4.7 harness
Locale mitigation plugin spawns codex with LANG/LC_ALL=en_US.UTF-8 (non-UTF-8 host locale notice fired)
sessionRuntime mode observed both shared (pipe IPC) and direct between sessions; no difference in outcome

Reproduction

  1. From Claude Code, run /opnd-codex:setup. Result:
    {
      "ready": true,
      "codex": { "available": true, "detail": "codex-cli 0.130.0; advanced runtime available" },
      "auth": { "available": true, "loggedIn": true, "verified": true, "source": "app-server", "authMethod": "chatgpt" }
    }
  2. From Claude Code, spawn a rescue agent (background or foreground) — e.g.
    Agent({subagent_type:"opnd-codex:codex-rescue", prompt:"<anything>"}).
  3. Agent returns:
    [codex-rescue] Codex was not invoked — the Bash call failed or was denied.
    Codex 인증 토큰이 만료되어 있습니다 (refresh token already used).
    
  4. User runs codex logout && codex login (or re-authenticates in Codex
    Desktop). setup --json reports green again.
  5. Spawn rescue agent again — same refresh token already used failure.
  6. Repeat 3 cycles. Same outcome each time.

Expected vs Actual

  • Expected: setup --json verified:true predicts that the next
    rescue-agent invocation can actually start a Codex session.
  • Actual: setup probes a different code path (auth cache / token presence)
    than the rescue agent uses (new session start → uses refresh token →
    reuse-detection error). The two paths disagree, so the green light is
    misleading.

Impact

  • Users waste cycles on codex logout && codex login thinking the host harness
    needs a nudge, then the very next call still fails.
  • Plugin tools that gate themselves on setup verified:true (e.g. router
    default-pair, skip-taxonomy false_positive_advisory reason) end up
    recommending Codex pair when the actual call cannot succeed.
  • For me specifically: a 5-turn investigation thread ran 3 retries before
    giving up; verification looked healthy each turn, so the harness kept trying.

Hypothesis

  • ChatGPT-auth refresh tokens appear to be single-use / rotated in
    codex-cli 0.130.0. setup happily reads the cache while it is still valid
    in the file system, but the moment the rescue agent triggers a fresh
    app-server session start, the refresh token has already been consumed by an
    earlier process (Codex Desktop, an earlier plugin call, or an outdated cache
    entry) and rejected.
  • The setup --json verified check needs to either:
    • actually issue a lightweight ChatGPT app-server probe that consumes a
      refresh-cycle the same way the rescue agent will, or
    • treat the auth cache as advisory only and downgrade verified:true to a
      softer signal (e.g. loggedIn:true, verification:cached-only).

Suggested next steps for maintainer

  1. Reproduce on a Windows + Git Bash + ChatGPT-auth host (the locale notice
    suggests this combination is already known).
  2. Decide whether setup --json should perform a real app-server round trip
    before declaring verified:true, or whether the verified field should
    be removed in favor of a softer signal.
  3. Document the failure mode in CHANGELOG / README so harness authors can
    choose a non-Codex fallback when 2+ recent rescue invocations have failed
    with refresh token already used in the same session.

Related logs

  • 3 consecutive failures captured locally; trace IDs available on request.
  • All failures emitted the same string: Your access token could not be refreshed because your refresh token was already used.
  • No actionsTaken reported by setup --json between the 3 cycles.

(이 파일은 사용자가 opnd-io/opnd-codex-plugin 의 이슈 기능을 활성화한 뒤
gh issue create --body-file <이 파일> --title ... 로 등록할 수 있도록 작성된
draft. 한글 + 영문 혼용 — 사용자 환경에 맞춰 가다듬어도 됨.)

Activity

  1. tgkim-openerd commented on May 28, 2026

    @tgkim-openerd
    Collaborator

    Additional reproduction — single-use refresh token rotation hypothesis confirmed (2026-05-28, follow-up)

    Captured a clean back-to-back trace in the same Claude Code session that confirms
    the hypothesis from the original report (setup --json advisory is a cached
    view of an already-rotated refresh token).

    Sequence

    1. opnd-codex:setup --json (after the user re-authenticated):
      ready: true
      loggedIn: true
      verified: true
      detail: ChatGPT login active for [email protected]
      sessionRuntime.mode: direct startup
      
    2. 1st rescue call — foreground, tiny prompt (Codex 연결 헬스 체크 / OK 만 출력하라):
      • ✅ Success. tool_uses:0, duration_ms:4150. Codex replied with OK / ....
    3. 2nd rescue call — background, large audit prompt (~3.5 KB, NOINDEX+tkmix audit):
      • ❌ Fail. Your access token could not be refreshed because your refresh token was already used.
    4. opnd-codex:setup --json again, immediately after the fail:
      ready: false
      loggedIn: false
      verified: null
      detail: The active provider requires OpenAI authentication
      sessionRuntime.mode: shared
      sessionRuntime.endpoint: pipe:\\.\pipe\cxc-4s5skG-codex-app-server
      

    What this shows

    • The 1st rescue call consumed and rotated the ChatGPT refresh token (single-use
      rotation policy on the OpenAI side).
    • The 2nd rescue call tried to start a new app-server session using the
      already-rotated token → reuse detection → 401.
    • Crucially, setup --json correctly reflects the broken state only after
      a failed call rolls the cache
      (step 4 reports loggedIn:false honestly).
      In step 1, setup --json read the cached "fresh after login" token and
      declared verified:true, but had no way to know the next rescue invocation
      would consume it.
    • The sessionRuntime.mode also flips: direct → shared once the first
      call leaves a pipe behind. The shared pipe survives the auth failure, so
      subsequent calls hit the dead pipe (issue [Bug] setup --json reports verified:true but rescue agent fails with 'refresh token already used' (false-positive advisory) #2 step 5 pattern).

    What the host harness saw

    Same as issue #2: the harness saw verified:true, started background work,
    got the same refresh token already used error, and had no signal in advance
    that the 2nd call would fail. Setup advisory remains a false-positive predictor.

    Suggested fix direction

    • setup --json should either:
      • actually issue a no-op app-server probe before declaring verified:true
        (consuming the refresh token slot the same way a real rescue call would),
      • or stop emitting verified:true entirely when authMethod=chatgpt,
        and instead emit verified:cached so harness authors know the green light
        is advisory only.
    • Alternatively, gate the shared sessionRuntime so that once a call fails
      with refresh token already used, the pipe is torn down and the next
      setup --json doesn't keep dangling a dead pipe in sessionRuntime.endpoint.

    (이 텍스트는 #2 코멘트로 그대로 붙여 쓸 수 있도록 영문 위주로 작성.)

  2. tgkim-openerd commented on Jun 8, 2026

    @tgkim-openerd
    Collaborator

    Root cause confirmed: v2.0.0 dual-home auth drift (not just a probe mismatch)

    Tracked this down on plugin 2.2.0 / codex-cli 0.134.0 / Opus 4.8 harness / Windows 11. The "setup green but rescue fails with refresh token already used" symptom is a direct consequence of the v2.0.0 home split, and it's fully reproducible from the on-disk state.

    What actually happens

    buildPluginCodexEnv() in scripts/lib/app-server.mjs (PR-5.6 / openai#282) injects a dedicated home into every plugin-spawned codex child:

    // app-server.mjs
    if (String(sanitizedBase.CODEX_PLUGIN_USE_DEFAULT_HOME ?? "").trim() === "1") {
      result = { ...sanitizedBase };                 // shared/default home
    } else if (sanitizedBase.CODEX_HOME?.trim()) {
      result = { ...sanitizedBase };                 // user-pinned home
    } else {
      const pluginCodexHome = path.join(home, ".codex", "claude-code");
      result = { ...sanitizedBase, CODEX_HOME: pluginCodexHome };  // <-- default path
    }

    So there are two independent auth.json files:

    File Used by Refreshed by
    ~/.codex/auth.json direct codex CLI, Codex Desktop, and setup --json's verified check codex login / Desktop re-auth
    ~/.codex/claude-code/auth.json rescue/agent invocations (buildPluginCodexEnv → CODEX_HOME) only when a plugin codex session actually runs

    When the user re-authenticates (codex logout && codex login or Desktop), only the root ~/.codex/auth.json rotates its refresh_token. The plugin-home copy still holds the previous refresh_token. The next rescue agent spawns codex with CODEX_HOME=~/.codex/claude-code/, tries to refresh using that now-superseded token, and ChatGPT's reuse-detection rejects it → refresh token already used. Meanwhile setup --json keeps reporting verified:true because it probes the root home, which is fine. That's the "false-positive advisory" in this issue — the two code paths read different homes, not just different cache layers.

    This also explains the infinite re-auth loop: every codex login only fixes root; the plugin home stays stale, so the very next rescue fails identically.

    Evidence (the plugin's own advisory already detects it)

    setup --json returns:

    "pluginHomeAdvisory": { "staleAuth": true, "staleAuthDeltaSec": 855305, "largeWalBytes": 155888536 }

    855305s ≈ 9.9 days, matching the two file mtimes exactly (root auth.json ~9.9 days newer than ~/.codex/claude-code/auth.json). So the data to predict the failure is already computed — it just isn't wired into the auth.verified verdict.

    Suggested fixes (in priority order)

    1. Make the setup --json verified check probe the same home the rescue path uses (buildPluginCodexEnv). This is the actual ask of this issue — stop the green light when the plugin home token is the one that will be used and is stale. Cheap version: fold pluginHomeAdvisory.staleAuth into auth.verified (verified:false when staleAuth and not USE_DEFAULT_HOME).
    2. Self-heal on broker start: when plugin-home auth is older than root auth, copy root→plugin before spawning (the advisory already computes staleAuth); or on a refresh token already used error, re-copy root auth and retry once.
    3. Document the single-home escape hatch more loudly: CODEX_PLUGIN_USE_DEFAULT_HOME=1 collapses both homes back to ~/.codex/, so there's only one token and no drift. Caveat worth documenting: it must be present in the env Claude Code hands to the broker — on Windows a freshly-setx/user-scope env var is not picked up by a mere VS Code window reload (the parent process tree keeps the old block); it needs a full sign-out/in, or set it via Claude Code settings.json "env" which is injected directly into spawned processes.

    Workarounds for anyone hitting this now

    • Immediate: cp ~/.codex/auth.json ~/.codex/claude-code/auth.json (the companion's own nextSteps[0]). Unblocks the next rescue, but re-drifts on the next token rotation.
    • Permanent, no drift: CODEX_PLUGIN_USE_DEFAULT_HOME=1 via settings.json "env" (then restart Claude Code so the broker re-spawns with it).

    Happy to PR fix #1 (folding staleAuth into the verified verdict) if that direction is acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions