@@ -3,6 +3,7 @@ use chrono::Utc;
33use serde:: { Deserialize , Serialize } ;
44use std:: fs:: File ;
55use std:: path:: { Path , PathBuf } ;
6+ use std:: process:: Command ;
67
78use super :: api:: { live_limits_from_usage_response, UsageResponse } ;
89use super :: cache:: disk_cache_path;
@@ -17,6 +18,11 @@ const CLAUDE_OAUTH_TOKEN_URL: &str = "https://platform.claude.com/v1/oauth/token
1718// Refresh proactively when within this many seconds of expiry so the next
1819// HTTP call always carries a fresh token (no wasted 401 round-trip).
1920const TOKEN_REFRESH_MARGIN_SECS : i64 = 60 ;
21+ #[ cfg( windows) ]
22+ const CREATE_NO_WINDOW : u32 = 0x08000000 ;
23+
24+ #[ cfg( windows) ]
25+ use std:: os:: windows:: process:: CommandExt ;
2026
2127#[ derive( Debug , Deserialize ) ]
2228struct ProfileResponse {
@@ -28,6 +34,16 @@ struct ProfileOrg {
2834 rate_limit_tier : Option < String > ,
2935}
3036
37+ #[ derive( Debug , Deserialize ) ]
38+ #[ serde( rename_all = "camelCase" ) ]
39+ struct AuthStatus {
40+ logged_in : bool ,
41+ email : Option < String > ,
42+ org_id : Option < String > ,
43+ subscription_type : Option < String > ,
44+ auth_method : Option < String > ,
45+ }
46+
3147#[ derive( Debug , Clone , Deserialize , Serialize ) ]
3248struct CredentialsFile {
3349 #[ serde( rename = "claudeAiOauth" ) ]
@@ -167,6 +183,9 @@ pub(crate) fn read_oauth_token() -> Result<String> {
167183}
168184
169185pub ( crate ) fn active_account_identity ( ) -> Option < AccountIdentity > {
186+ if let Some ( identity) = active_auth_status_identity ( ) {
187+ return Some ( identity) ;
188+ }
170189 let ( _, creds) = read_credentials ( ) . ok ( ) ?;
171190 crate :: account:: token_identity (
172191 "claude" ,
@@ -175,6 +194,57 @@ pub(crate) fn active_account_identity() -> Option<AccountIdentity> {
175194 )
176195}
177196
197+ fn active_auth_status_identity ( ) -> Option < AccountIdentity > {
198+ let mut cmd = Command :: new ( "claude" ) ;
199+ #[ cfg( windows) ]
200+ cmd. creation_flags ( CREATE_NO_WINDOW ) ;
201+ let output = cmd. args ( [ "auth" , "status" , "--json" ] ) . output ( ) . ok ( ) ?;
202+ if !output. status . success ( ) {
203+ return None ;
204+ }
205+ let status: AuthStatus = serde_json:: from_slice ( & output. stdout ) . ok ( ) ?;
206+ if !status. logged_in {
207+ return None ;
208+ }
209+ let mut parts = Vec :: new ( ) ;
210+ if let Some ( org_id) = status
211+ . org_id
212+ . as_deref ( )
213+ . map ( str:: trim)
214+ . filter ( |s| !s. is_empty ( ) )
215+ {
216+ parts. push ( format ! ( "org:{org_id}" ) ) ;
217+ }
218+ if let Some ( email) = status
219+ . email
220+ . as_deref ( )
221+ . map ( str:: trim)
222+ . filter ( |s| !s. is_empty ( ) )
223+ {
224+ parts. push ( format ! ( "email:{}" , email. to_ascii_lowercase( ) ) ) ;
225+ }
226+ if parts. is_empty ( ) {
227+ if let Some ( method) = status
228+ . auth_method
229+ . as_deref ( )
230+ . map ( str:: trim)
231+ . filter ( |s| !s. is_empty ( ) )
232+ {
233+ parts. push ( format ! ( "auth:{method}" ) ) ;
234+ }
235+ }
236+ if parts. is_empty ( ) {
237+ return None ;
238+ }
239+ let raw = parts. join ( "|" ) ;
240+ let source = status
241+ . subscription_type
242+ . as_deref ( )
243+ . map ( |sub| format ! ( "claude-auth-status:{sub}" ) )
244+ . unwrap_or_else ( || "claude-auth-status" . to_string ( ) ) ;
245+ crate :: account:: explicit_identity ( "claude" , & raw , & source)
246+ }
247+
178248/// Lightweight availability probe: does a parseable credentials file with an
179249/// access token exist on disk? Does NOT hit the network — refresh logic lives
180250/// in `read_oauth_token` / `ensure_fresh_access_token` and only runs when we
0 commit comments