From b0f5451fdf02312b4a16d696888ec946c2afbf09 Mon Sep 17 00:00:00 2001 From: Chris Kaubisch Date: Mon, 2 Nov 2026 20:00:20 +0100 Subject: [PATCH] fix(claude-local): skip the 2.1.291 copy hint in the setup-token prompt parser (#15550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - AI connections let a user connect a Claude subscription for all agents of an organization > - In a Docker deployment the server runs `claude setup-token` in a PTY and parses its output for the sign-in URL (`server/src/services/local-ai-browser-login.ts`, `packages/adapters/claude-local/src/server/setup-token-parse.ts`) > - The Docker image installs `@anthropic-ai/claude-code@latest`, and `claude` 2.1.291 adds a copy hint line between the URL and the code prompt > - The parser requires the prompt on the first non-blank line after the URL, so it returns null and the UI waits without an error > - This pull request lets the parser skip that exact hint line > - The benefit is that Claude subscription sign-in works again with the current CLI ## Linked Issues or Issue Description No issue exists. I searched open and closed issues and pull requests for "Preparing browser sign-in" and the setup-token parser. **What happened?** In the "Claude - Subscription" connection dialog, the UI shows "Preparing browser sign-in…" and "Start sign-in again". It shows no sign-in link and no error. "Start sign-in again" starts the same wait again. After 300 s the run fails with "Sign-in ended before the account was connected". **Expected behavior** The dialog shows the sign-in link and the code field a few seconds after the start. **Steps to reproduce** 1. Run the Docker image with `claude` 2.1.291 (`@latest` at the time of writing). 2. Open the AI connection dialog and select "Claude - Subscription". 3. The dialog stays on "Preparing browser sign-in…". **Paperclip version or commit** `master` at 03cf6a6ec (includes #14970). `claude --version`: 2.1.291. **Deployment mode** Authenticated, private exposure, Docker. **Relevant logs or output** The server logs nothing for this flow. A manual run in the container shows the output that the parser reads (URL values removed): ``` docker exec -it -u node sh -c 'CLAUDE_CONFIG_DIR=$(mktemp -d) CLAUDE_CODE_OAUTH_TOKEN= ANTHROPIC_API_KEY= ANTHROPIC_AUTH_TOKEN= BROWSER=true claude setup-token' Browser didn't open? Use the url below to sign in (c to copy) https://claude.com/cai/oauth/authorize?code=true&client_id=…&response_type=code&redirect_uri=ht tps%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2Fcallback&scope=user%3Ainference&code_challenge=… …&code_challenge_method=S256&state=… Hold Shift while selecting to use your terminal's native copy Paste code here if prompted > ``` `findBrowserCodePrompt` reads the first non-blank, non-URL line after the URL. Here that line is the copy hint, so the parser returns null. The runner never calls `onPrompt`, and `check()` returns `sign_in_required` with no `authorizationUrl` and no error. ## What Changed - `setup-token-parse.ts`: add `COPY_HINT_LINE_RE`, which matches only the exact copy hint (CHA-spaced words and a curly apostrophe are allowed). `findBrowserCodePrompt` skips a line that matches it. The prompt line rules do not change. - `setup-token-parse.test.ts`: add a test with the 2.1.291 layout (hard-wrapped URL, CHA-spaced hint, blank lines, prompt). ## Verification - `bun test packages/adapters/claude-local/src/server/setup-token-parse.test.ts packages/adapters/claude-local/src/server/setup-token-characterization.test.ts`: 71 pass, 1 skip, 0 fail. - The 2.1.291 output above returns `null` before the change and the URL plus prompt after the change. - The test "returns null when a code-like value sits on the line after the URL" still passes. Other prose between the URL and the prompt still fails closed. ## Risks - Low risk. The change skips one fixed line and binds no value from it. - A later CLI version can change the hint text again. The flow then hangs the same way until the 300 s timeout. An earlier error (for example, when no prompt appears after 60 s) and a `log` option for `runSetupTokenLogin` in `local-ai-browser-login.ts` would make this visible. This pull request does not include them. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use (code search, shell, test runs). A human ran the container commands and reviewed the change. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Claude Opus 5.5 --- .../src/server/setup-token-parse.test.ts | 8 ++++++++ .../claude-local/src/server/setup-token-parse.ts | 13 ++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/adapters/claude-local/src/server/setup-token-parse.test.ts b/packages/adapters/claude-local/src/server/setup-token-parse.test.ts index b703188062f..3f3df53388f 100644 --- a/packages/adapters/claude-local/src/server/setup-token-parse.test.ts +++ b/packages/adapters/claude-local/src/server/setup-token-parse.test.ts @@ -337,6 +337,14 @@ describe("parseSetupTokenPrompt", () => { expect(parseSetupTokenPrompt(completeOutput(withKey))).toBeNull(); }); + it("skips the 2.1.291 copy hint between the URL and the prompt", () => { + const hint = chaSpaced("Hold Shift while selecting to use your terminal's native copy".split(" ")); + const text = [PREAMBLE_LINE, hardWrap(CLAUDE_COM_URL, 130), "", hint, "", "", PROMPT_LINE].join("\n"); + const result = parseSetupTokenPrompt(text); + expect(result?.url).toBe(CLAUDE_COM_URL); + expect(result?.prompt).toBe(SETUP_TOKEN_PROMPT); + }); + it("returns null when a code-like value sits on the line after the URL", () => { // The line right after the URL must hold the browser-code prompt. A code-like // value on that line cannot bind, so the parser returns null. diff --git a/packages/adapters/claude-local/src/server/setup-token-parse.ts b/packages/adapters/claude-local/src/server/setup-token-parse.ts index d845f5ab348..6028695d16a 100644 --- a/packages/adapters/claude-local/src/server/setup-token-parse.ts +++ b/packages/adapters/claude-local/src/server/setup-token-parse.ts @@ -181,6 +181,10 @@ const URL_PREAMBLE_RE = /the url below to sign in/i; // the line. const PROMPT_LINE_RE = /^Paste code here if prompted\b[\s>*]*$/; +// The copy hint that `claude` 2.1.291 prints between the URL block and the +// prompt. The parser skips this exact line and binds no value from it. +const COPY_HINT_LINE_RE = /^Hold\s+Shift\s+while\s+selecting\s+to\s+use\s+your\s+terminal['’]s\s+native\s+copy\.?$/i; + // The maximum number of characters between the end of the URL preamble line and // the start of the URL. The Claude UI prints the URL right after the preamble. // The parser accepts a URL start only inside this window, so a URL far from the @@ -363,9 +367,11 @@ function reassembleUrl(lines: string[], startLine: number): SetupTokenUrlMatch | * line after the URL block. The parser reads the non-blank lines after the URL * end line. The Claude UI can emit one full URL line per wrapped display row, so * it repeats the same full authorization URL on a few consecutive lines. The - * parser skips a line that repeats `url`, then binds the prompt on the first - * non-blank, non-repeat line. That line must match the exact prompt shape. - * Returns null when the first non-blank, non-repeat line is not the prompt. + * parser skips a line that repeats `url`. The `claude` 2.1.291 UI also prints a + * copy hint before the prompt, so the parser skips a line that matches {@link + * COPY_HINT_LINE_RE}. The parser then binds the prompt on the first non-blank + * line that is not a repeat and not the copy hint. That line must match the + * exact prompt shape. Returns null when that line is not the prompt. */ function findBrowserCodePrompt(lines: string[], endLine: number, url: string): string | null { for (let i = endLine + 1; i < lines.length; i += 1) { @@ -375,6 +381,7 @@ function findBrowserCodePrompt(lines: string[], endLine: number, url: string): s // punctuation first, the same way the repeated-URL comparison needs, so a // repeated URL with a trailing punctuation mark still matches. if (trimmed.replace(TRAILING_PUNCTUATION_RE, "") === url) continue; + if (COPY_HINT_LINE_RE.test(trimmed)) continue; return PROMPT_LINE_RE.test(trimmed) ? SETUP_TOKEN_PROMPT : null; } return null;