Skip to content

fix(server): prevent probeClaudeCapabilities from wasting API requests#2192

Merged
juliusmarminge merged 4 commits intopingdotgg:mainfrom
reasv:fix/probe-capabilities-api-leak
Apr 20, 2026
Merged

fix(server): prevent probeClaudeCapabilities from wasting API requests#2192
juliusmarminge merged 4 commits intopingdotgg:mainfrom
reasv:fix/probe-capabilities-api-leak

Conversation

@reasv
Copy link
Copy Markdown
Contributor

@reasv reasv commented Apr 19, 2026

Fixes #2191

Issue: Unintended API token consumption (~tens of thousands of tokens every 5 minutes) when t3code is running with Claude Code configured, even with no browser open and no user interaction.

Cause: probeClaudeCapabilities() silently sends a real prompt to the Anthropic messages API every 5 minutes whenever t3code is running with Claude Code configured, even with no browser open. Each request consumes tens of thousands of tokens (full system prompt + tools). This is caused by two interacting bugs: the SDK ignoring maxTurns: 0 (JS falsiness), and query() writing the prompt to subprocess stdin before initializationResult() is awaited.

What Changed

Replace the string prompt "." with a never-yielding AsyncIterable<SDKUserMessage> so no user message is ever written to the Claude subprocess stdin.
Remove the ineffective maxTurns: 0 option.

Why

The previous approach relied on aborting the subprocess after initializationResult() resolved, but the SDK's query() writes string prompts to stdin synchronously at construction time, before the abort can fire. Using an async iterable that never yields is the minimal change that prevents any prompt from reaching the subprocess while still allowing the local initialization IPC to complete normally.

Checklist

  • [x ] This PR is small and focused
  • I explained what changed and why

Note

Medium Risk
Touches Claude provider capability probing by changing how the SDK subprocess is initialized; low surface area but could affect subscription/command detection if the SDK expects an initial prompt.

Overview
Prevents the Claude provider’s periodic probeClaudeCapabilities() from inadvertently starting real Anthropic API requests during capability detection.

The probe now passes a never-yielding AsyncIterable<SDKUserMessage> (plus a small waitForAbortSignal helper) so nothing is written to the Claude subprocess stdin, and it drops the previously ineffective maxTurns: 0 option.

Reviewed by Cursor Bugbot for commit 2df510b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix probeClaudeCapabilities to avoid sending API requests to the Claude subprocess

  • Replaces the string prompt "." with a never-yielding async generator so no user message is sent to the Claude subprocess during capability probing.
  • Adds a waitForAbortSignal helper in ClaudeProvider.ts that resolves when an AbortSignal fires, used to cleanly exit the prompt generator after initialization data is read.
  • Removes maxTurns: 0 from the claudeQuery options, relying on the abort mechanism to end the session instead.

Macroscope summarized 2df510b.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 19, 2026 00:17
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 19, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ad0d4bc0-e09e-4c45-b933-027e9ca9feb6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Apr 19, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents probeClaudeCapabilities() from unintentionally consuming Anthropic API tokens while the server is idle by ensuring no prompt is ever written to the Claude Code subprocess stdin.

Changes:

  • Replaces the probe prompt "." with a never-yielding AsyncIterable<SDKUserMessage> so the SDK doesn’t synchronously write a user message to stdin.
  • Removes the ineffective maxTurns: 0 option from the Claude Agent SDK query options.
  • Updates the probe’s inline documentation to reflect the new “no prompt is sent” strategy.
Comments suppressed due to low confidence (1)

apps/server/src/provider/Layers/ClaudeProvider.ts:515

  • This change is security/ops sensitive (prevents unintended API token burn) but probeClaudeCapabilities isn’t covered by tests. Please add a unit test that asserts the probe passes a non-string prompt (so nothing is written to stdin) and that the probe completes/aborts as expected under timeout/abort conditions (can be done by mocking @anthropic-ai/claude-agent-sdk’s query).
const probeClaudeCapabilities = (binaryPath: string) => {
  const abort = new AbortController();
  return Effect.tryPromise(async () => {
    const q = claudeQuery({
      prompt: (async function* (): AsyncGenerator<SDKUserMessage> {
        // Never yield — we only need initialization data, not a conversation.
        // This prevents any prompt from reaching the Anthropic API.
        await new Promise<never>(() => {});
      })(),
      options: {
        persistSession: false,
        pathToClaudeCodeExecutable: binaryPath,
        abortController: abort,
        settingSources: ["user", "project", "local"],
        allowedTools: [],
        stderr: () => {},
      },
    });
    const init = await q.initializationResult();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

prompt: (async function* (): AsyncGenerator<SDKUserMessage> {
// Never yield — we only need initialization data, not a conversation.
// This prevents any prompt from reaching the Anthropic API.
await new Promise<never>(() => {});
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await new Promise<never>(() => {}) creates a non-terminating async generator that can’t be cleanly closed on abort. If the SDK tries to dispose of the prompt iterable (e.g., awaiting iterator.return() during shutdown), this can stall cleanup. Consider awaiting the AbortController signal instead so the iterable completes immediately after abort.abort() is called (still yielding no messages).

Suggested change
await new Promise<never>(() => {});
await new Promise<void>((resolve) => {
if (abort.signal.aborted) {
resolve();
return;
}
abort.signal.addEventListener("abort", () => resolve(), { once: true });
});

Copilot uses AI. Check for mistakes.
macroscopeapp[bot]
macroscopeapp bot previously approved these changes Apr 19, 2026
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 19, 2026

Approvability

Verdict: Needs human review

Changes how the probe function interacts with the Claude SDK, replacing a simple string prompt with a never-yielding async generator. While intended as an optimization to prevent wasted API requests, this alters the SDK interaction pattern and warrants verification that the SDK handles this input correctly.

You can customize Macroscope's approvability policy. Learn more.

- wait on abort instead of a never-settling promise
- add tests for abort-signal behavior
@macroscopeapp macroscopeapp bot dismissed their stale review April 19, 2026 17:48

Dismissing prior approval to re-evaluate 859785f

- Keep Claude probe prompt from yielding any user content
- Avoid sending initialization-only probe data to Anthropic
macroscopeapp[bot]
macroscopeapp bot previously approved these changes Apr 19, 2026
@macroscopeapp macroscopeapp bot dismissed their stale review April 20, 2026 00:05

Dismissing prior approval to re-evaluate 2df510b

@juliusmarminge juliusmarminge enabled auto-merge (squash) April 20, 2026 00:06
@juliusmarminge juliusmarminge merged commit 8dbcf92 into pingdotgg:main Apr 20, 2026
12 checks passed
aaditagrawal added a commit to aaditagrawal/t3code that referenced this pull request Apr 20, 2026
Upstream additions:
- fix(web): restore manual sort drag and keep per-group expand state (pingdotgg#2221)
- fix: Change right panel sheet to be below title bar / action bar (pingdotgg#2224)
- Refactor OpenCode lifecycle and structured output handling (pingdotgg#2218)
- effect-codex-app-server (pingdotgg#1942)
- Redesign model picker with favorites and search (pingdotgg#2153)
- fix(server): prevent probeClaudeCapabilities from wasting API requests (pingdotgg#2192)
- fix(server): handle OpenCode text response format in commit message gen (pingdotgg#2202)
- Devcontainer / IDE updates (pingdotgg#2208)
- Expand leading ~ in Codex home paths before exporting CODEX_HOME (pingdotgg#2210)
- fix(release): use v<semver> tag format for nightly releases (pingdotgg#2186)

Fork adaptations:
- Took upstream's redesigned model picker with favorites and search
- Removed deleted codexAppServerManager (replaced by effect-codex-app-server)
- Stubbed fetchCodexUsage (manager-based readout no longer available)
- Extended PROVIDER_ICON_BY_PROVIDER for all 8 fork providers
- Extended modelOptionsByProvider test fixtures for all 8 providers
- Inline ClaudeSlashCommand type (not yet re-exported from SDK)
- Updated SettingsPanels imports for new picker module structure
- Preserved fork's CI customizations (ubuntu-24.04 not Blacksmith)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Nightly: Claude Code burns tokens every 5 minutes while t3code is running idle

3 participants