diff --git a/debug_claude.py b/debug_claude.py index 02c36e4..ed653b2 100644 --- a/debug_claude.py +++ b/debug_claude.py @@ -1,7 +1,7 @@ -"""Debug script — tests Claude API connectivity.""" +"""Debug script — tests Claude API connectivity using curl_cffi Chrome impersonation.""" import os from dotenv import load_dotenv -import requests +from curl_cffi import requests as curl_requests load_dotenv() key = os.getenv("CLAUDE_SESSION_KEY") @@ -9,16 +9,14 @@ if not key: print("ERROR: CLAUDE_SESSION_KEY not found in .env") raise SystemExit(1) -s = requests.Session() -# Test 1: cookie as a jar entry (correct way) +s = curl_requests.Session(impersonate="chrome120") s.cookies.set("sessionKey", key, domain="claude.ai", path="/") s.headers.update({ - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", "Referer": "https://claude.ai/", "Accept": "application/json", }) -print("Calling /api/organizations (cookie jar) ...") +print("Calling /api/organizations (with Chrome TLS impersonation) ...") r = s.get("https://claude.ai/api/organizations", timeout=15) print(f"Status: {r.status_code}") -print(f"Response (first 300 chars): {r.text[:300]}") +print(f"Response (first 400 chars): {r.text[:400]}") diff --git a/src/providers/claude.py b/src/providers/claude.py index e266e8c..47ae18b 100644 --- a/src/providers/claude.py +++ b/src/providers/claude.py @@ -3,25 +3,35 @@ import logging import os +from curl_cffi import requests as curl_requests + from src.providers.base import BaseProvider, ProviderError logger = logging.getLogger(__name__) BASE_URL = "https://claude.ai/api" +IMPERSONATE = "chrome120" class ClaudeProvider(BaseProvider): """Provider for Claude conversations via the internal web API. - Authentication: Cookie: sessionKey= - Token: sessionKey cookie value from claude.ai. - Typical validity: ~30 days (opaque; expiry cannot be decoded client-side). + Uses curl_cffi to impersonate Chrome's TLS fingerprint, bypassing + Cloudflare's bot detection (same issue as chatgpt.com). + + Authentication: sessionKey cookie (~30 day lifetime, opaque string). + Expiry cannot be decoded client-side — a 401 is the only signal. """ provider_name = "claude" def __init__(self, session_key: str | None = None) -> None: - super().__init__() + cf_session = curl_requests.Session(impersonate=IMPERSONATE) + super().__init__(session=cf_session) # type: ignore[arg-type] + + # Remove base class User-Agent so curl_cffi uses its Chrome-matched UA + self._session.headers.pop("User-Agent", None) + key = session_key or os.getenv("CLAUDE_SESSION_KEY", "").strip() if not key: raise ProviderError( @@ -32,7 +42,7 @@ class ClaudeProvider(BaseProvider): "Run 'python -m src.main auth' to configure it." ), ) - # Set sessionKey in the cookie jar (not as a raw header string) + # Set sessionKey in the cookie jar self._session.cookies.set("sessionKey", key, domain="claude.ai", path="/") self._session.headers.update( { @@ -41,7 +51,7 @@ class ClaudeProvider(BaseProvider): } ) self._org_id: str | None = None # cached per session - logger.debug("[claude] Session initialised (key: [REDACTED])") + logger.debug("[claude] Session initialised with Chrome TLS impersonation (key: [REDACTED])") def _handle_401(self) -> None: msg = (