Skip to content

shutil.which() blocking call causes BlockingError in async environments #324

@torykit

Description

@torykit

shutil.which blocking call causes BlockingError in async environments

Environment

  • Platform: macOS
  • Python Version: 3.13
  • Claude Agent SDK Version: latest
  • Claude CLI: Installed at /Users/zhao/.nvm/versions/node/v20.19.4/bin/claude

Bug Description

The Claude Agent SDK fails when used in async environments due to a synchronous blocking call to shutil.which() in the subprocess CLI transport initialization. The blocking call is detected and prevented by the blockbuster library, which monitors async environments to protect the event loop from blocking operations.

Steps to Reproduce

  1. Create a LangGraph application that uses the Claude Agent SDK
  2. Use the query() function without explicitly providing cli_path parameter
  3. Run the application with langgraph dev
  4. The error occurs during SDK initialization when it tries to find the Claude CLI path

Minimal Reproduction Code:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
from langchain_core.messages import BaseMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.constants import START
from langgraph.graph.message import add_messages
from langgraph.graph.state import StateGraph
from typing import Annotated, TypedDict

class ClaudeRunState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]

async def claude_run_task(state: ClaudeRunState):
    options = ClaudeAgentOptions(
        system_prompt="You are an expert Python developer",
        permission_mode="acceptEdits",
        cwd="/path/to/project",
        # Note: cli_path not provided - triggers shutil.which() call
    )

    async for message in query(prompt="这个项目是做什么的?", options=options):
        print(message)

# LangGraph setup
builder = StateGraph(ClaudeRunState)
builder.add_node("claude_run_task", claude_run_task)
builder.add_edge(START, "claude_run_task")
graph = builder.compile()

# Run with langgraph dev

Expected Behavior

The SDK should either:

  1. Use async-compatible methods to find the CLI path, or
  2. Allow the blocking call to be bypassed when cli_path is provided, or
  3. Provide clear documentation about async environment requirements

Actual Behavior

blockbuster.blockbuster.BlockingError: Blocking call to os.access

Heads up! LangGraph dev identified a synchronous blocking call in your code. When running in an ASGI web server, blocking calls can degrade performance for everyone since they tie up the event loop.

Root Cause Analysis

The issue occurs in claude_agent_sdk/_internal/transport/subprocess_cli.py:71:

def _find_cli(self) -> Path:
    if cli := shutil.which("claude"):  # ← Synchronous blocking call
        return Path(cli)
    # ... rest of method

This synchronous shutil.which() call blocks the async event loop, which is prohibited in async environments monitored by the blockbuster library.

Workarounds

Provide the cli_path parameter:

options = ClaudeAgentOptions(
    cli_path="/Users/zhao/.nvm/versions/node/v20.19.4/bin/claude",
    # ... other options
)

Additional Context

  • This issue only affects async environments like LangGraph
  • The Claude CLI is properly installed and functional
  • The error occurs during SDK initialization, not during actual Claude interaction
  • This prevents the Claude Agent SDK from being used in popular async frameworks

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions