fix(server): prevent probeClaudeCapabilities from wasting API requests#2192
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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-yieldingAsyncIterable<SDKUserMessage>so the SDK doesn’t synchronously write a user message to stdin. - Removes the ineffective
maxTurns: 0option 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
probeClaudeCapabilitiesisn’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’squery).
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>(() => {}); |
There was a problem hiding this comment.
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).
| await new Promise<never>(() => {}); | |
| await new Promise<void>((resolve) => { | |
| if (abort.signal.aborted) { | |
| resolve(); | |
| return; | |
| } | |
| abort.signal.addEventListener("abort", () => resolve(), { once: true }); | |
| }); |
ApprovabilityVerdict: 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
Dismissing prior approval to re-evaluate 859785f
- Keep Claude probe prompt from yielding any user content - Avoid sending initialization-only probe data to Anthropic
Dismissing prior approval to re-evaluate 2df510b
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)
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 ignoringmaxTurns: 0(JS falsiness), and query() writing the prompt to subprocess stdin beforeinitializationResult()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: 0option.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
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 smallwaitForAbortSignalhelper) so nothing is written to the Claude subprocess stdin, and it drops the previously ineffectivemaxTurns: 0option.Reviewed by Cursor Bugbot for commit 2df510b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix
probeClaudeCapabilitiesto avoid sending API requests to the Claude subprocess"."with a never-yielding async generator so no user message is sent to the Claude subprocess during capability probing.waitForAbortSignalhelper inClaudeProvider.tsthat resolves when anAbortSignalfires, used to cleanly exit the prompt generator after initialization data is read.maxTurns: 0from theclaudeQueryoptions, relying on the abort mechanism to end the session instead.Macroscope summarized 2df510b.