Fix shared Claude auth: whole sign-in URL, recoverable rejected code #16

Merged
jknapp merged 1 commits from fix/shared-claude-auth-url-and-retry into main 2026-08-10 17:58:57 +00:00
Owner

Two bugs reported from real use: the sign-in URL was truncated, and after pasting the code the flow never completed.

Truncated URL. claude setup-token emits the URL as an OSC 8 hyperlink — the full 346-char target lives in the escape sequence's parameter, and the CLI emits one complete hyperlink per display line. Our ANSI stripper discarded OSC sequences entirely, so extractSignInUrl only ever saw the sliced visible text and its whitespace-terminated regex stopped mid-parameter. The stripper now extracts the OSC 8 target; the old scraping is kept as a fallback for CLI versions that don't emit one. stty cols also raised 200 → 400.

Never completes. Submitting the code works — verified both single-write and split-write against the real CLI. What hung was the state after: on a bad code the CLI prints Press Enter to retry and blocks, and nothing recognised it, so the exec sat until the 15-minute timeout while the UI said "Finishing sign-in". Rejection is now detected, the blocking Enter is sent, and the user gets a distinct "Code rejected — try again" state, bounded at 3 attempts. The two bugs compounded: the truncated URL is what produced an invalid code.

A token leak found along the way. The redactor masked the marker-bearing first line of a credential and printed the tail in clear. Closed, with the reassembly shared between the parser and the redactor so they cannot disagree.

Also fixes ESC ( B being mis-parsed as a 2-byte escape, leaking a stray B that could glue onto a token and make the parser reject it.

Verification. The real setup-token byte stream was captured under a pty at 80 and 400 columns, then replayed through the new Rust code in 37-byte chunks: all 10 OSC 8 targets recovered intact, rejection detected, no stray charset bytes. Token-wrap handling is synthetic-only — minting a real token needs a real login.

209 Rust tests, 283 frontend tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k

Two bugs reported from real use: the sign-in URL was truncated, and after pasting the code the flow never completed. **Truncated URL.** `claude setup-token` emits the URL as an **OSC 8 hyperlink** — the full 346-char target lives in the escape sequence's parameter, and the CLI emits one complete hyperlink per display line. Our ANSI stripper discarded OSC sequences entirely, so `extractSignInUrl` only ever saw the sliced visible text and its whitespace-terminated regex stopped mid-parameter. The stripper now extracts the OSC 8 target; the old scraping is kept as a fallback for CLI versions that don't emit one. `stty cols` also raised 200 → 400. **Never completes.** Submitting the code works — verified both single-write and split-write against the real CLI. What hung was the state *after*: on a bad code the CLI prints `Press Enter to retry` and blocks, and nothing recognised it, so the exec sat until the 15-minute timeout while the UI said "Finishing sign-in". Rejection is now detected, the blocking Enter is sent, and the user gets a distinct "Code rejected — try again" state, bounded at 3 attempts. The two bugs compounded: the truncated URL is what produced an invalid code. **A token leak found along the way.** The redactor masked the marker-bearing first line of a credential and printed the *tail* in clear. Closed, with the reassembly shared between the parser and the redactor so they cannot disagree. Also fixes `ESC ( B` being mis-parsed as a 2-byte escape, leaking a stray `B` that could glue onto a token and make the parser reject it. **Verification.** The real `setup-token` byte stream was captured under a pty at 80 and 400 columns, then replayed through the new Rust code in 37-byte chunks: all 10 OSC 8 targets recovered intact, rejection detected, no stray charset bytes. Token-wrap handling is synthetic-only — minting a real token needs a real login. 209 Rust tests, 283 frontend tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
jknapp added 1 commit 2026-08-10 17:16:41 +00:00
Fix the shared Claude auth flow: whole sign-in URL, recoverable rejected code
Build App / compute-version (pull_request) Successful in 6s
Build App / build-macos (pull_request) Successful in 2m30s
Build App / build-windows (pull_request) Successful in 5m23s
Build App / build-linux (pull_request) Successful in 5m50s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
c3f92674b1
Two compounding bugs made `claude setup-token` unusable, both measured against
2.1.226 under a pty rather than reasoned about.

**The sign-in URL was truncated.** The CLI emits it as an OSC 8 hyperlink and
slices the *visible* text of that hyperlink to the terminal width: a 346
character URL arrives at 80 columns as five separate hyperlink emissions, each
carrying the whole URL in its parameter and 80 characters of it on screen. The
transcript scraper picked up the first slice — a URL that parses, points at
claude.com, and cannot authorise anything. The ANSI stripper now surfaces the
OSC 8 target and `claude-token-link` carries it to the UI, which prefers it over
the scraped text. It still goes through `sanitizeRelayUrl` with the
ANTHROPIC_SIGN_IN_HOSTS allowlist before display and again before `openUrl` — an
OSC 8 parameter is never rendered, which makes it the easier place to hide a
hostile host, not a trusted one. The wrapped-display fallback is kept for CLI
versions that print a bare URL.

**A rejected code hung the flow.** On a bad paste the CLI prints `OAuth error:
Invalid code…` / `Press Enter to retry.` and blocks on stdin instead of exiting;
nothing recognised that, so the exec sat until the 15-minute timeout with the UI
still saying "Finishing sign-in". Given the first bug handed the user a truncated
URL, an invalid code was the likely first outcome. The streamed output is now
scanned for that message, `claude-token-code-rejected` reopens the input with an
explanation, and the Enter is sent so the next code has a prompt to land in —
bounded by MAX_CODE_ATTEMPTS, after which the flow reports a failure. An
undeterminable exec exit status is logged rather than silently read as success.

**A wrapped token was rejected *and* leaked.** `stty cols` fails silently, and an
80-column fallback splits the ~103 character token across two lines: the parser
saw a too-short fragment and failed, while the redactor masked the first line —
which carries the `sk-ant-` marker — and printed the second, the tail of a live
credential, to the UI in clear. `scan_credential_body` now reassembles a run
across hard wraps and both the parser and the redactor use it, so they cannot
disagree about where a credential ends. A join only happens across a break at a
plausible terminal margin (>= 40 columns) and only for a run not already long
enough to be a whole credential — without that second guard a repainting TUI
welds one frame's token onto the next frame's first word. The length floor is
applied to the reassembled body, so a fragment is still never accepted.

Also: `stty cols` raised 200 -> 400 (the URL alone needs ~350), and `ESC ( B` is
handled as the three-byte charset designation it is — it prefixes every repaint
frame, and treating it as two bytes emitted a stray `B` that could glue itself
onto a token and make the parser refuse it.

`submit_claude_token_code`'s single-write behaviour is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
jknapp merged commit 31c73adb13 into main 2026-08-10 17:58:57 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: CyberCoveLLC/Triple-C#16