@@ -193,8 +193,14 @@ def get_cached_credentials(self):
193193 return None
194194
195195 creds = json .loads (creds_json )
196+
197+ # Check for dummy/cleared credentials first
198+ # These are set when credentials are cleared to maintain keychain permissions
199+ if creds .get ("AccessKeyId" ) == "EXPIRED" :
200+ self ._debug_print ("Found cleared dummy credentials, need re-authentication" )
201+ return None
196202
197- # Validate expiration
203+ # Validate expiration for real credentials
198204 exp_str = creds .get ("Expiration" )
199205 if exp_str :
200206 exp_time = datetime .fromisoformat (exp_str .replace ("Z" , "+00:00" ))
@@ -220,8 +226,13 @@ def get_cached_credentials(self):
220226 try :
221227 with open (session_file , "r" ) as f :
222228 creds = json .load (f )
229+
230+ # Check for dummy/cleared credentials first
231+ if creds .get ("AccessKeyId" ) == "EXPIRED" :
232+ self ._debug_print ("Found cleared dummy credentials in session file, need re-authentication" )
233+ return None
223234
224- # Validate expiration
235+ # Validate expiration for real credentials
225236 exp_str = creds .get ("Expiration" )
226237 if exp_str :
227238 exp_time = datetime .fromisoformat (exp_str .replace ("Z" , "+00:00" ))
@@ -260,6 +271,66 @@ def save_credentials(self, credentials):
260271
261272 # Set restrictive permissions
262273 session_file .chmod (0o600 )
274+
275+ def clear_cached_credentials (self ):
276+ """Clear all cached credentials for this profile"""
277+ cleared_items = []
278+
279+ # Clear from keyring by replacing with expired credentials
280+ # This maintains keychain access permissions on macOS
281+ try :
282+ if keyring .get_password ("claude-code-with-bedrock" , f"{ self .profile } -credentials" ):
283+ # Replace with expired dummy credential instead of deleting
284+ # This prevents macOS from asking for "Always Allow" again
285+ expired_credential = json .dumps ({
286+ "Version" : 1 ,
287+ "AccessKeyId" : "EXPIRED" ,
288+ "SecretAccessKey" : "EXPIRED" ,
289+ "SessionToken" : "EXPIRED" ,
290+ "Expiration" : "2000-01-01T00:00:00Z" # Far past date
291+ })
292+ keyring .set_password ("claude-code-with-bedrock" , f"{ self .profile } -credentials" , expired_credential )
293+ cleared_items .append ("keyring credentials" )
294+ except Exception as e :
295+ self ._debug_print (f"Could not clear keyring credentials: { e } " )
296+
297+ # Clear monitoring token from keyring
298+ try :
299+ if keyring .get_password ("claude-code-with-bedrock" , f"{ self .profile } -monitoring" ):
300+ # Replace with expired dummy token
301+ expired_token = json .dumps ({
302+ "token" : "EXPIRED" ,
303+ "expires" : 0 , # Expired timestamp
304+ "email" : "" ,
305+ "profile" : self .profile
306+ })
307+ keyring .set_password ("claude-code-with-bedrock" , f"{ self .profile } -monitoring" , expired_token )
308+ cleared_items .append ("keyring monitoring token" )
309+ except Exception as e :
310+ self ._debug_print (f"Could not clear keyring monitoring token: { e } " )
311+
312+ # Clear session files
313+ session_dir = Path .home () / ".claude-code-session"
314+ if session_dir .exists ():
315+ session_file = session_dir / f"{ self .profile } -session.json"
316+ monitoring_file = session_dir / f"{ self .profile } -monitoring.json"
317+
318+ if session_file .exists ():
319+ session_file .unlink ()
320+ cleared_items .append ("session file" )
321+
322+ if monitoring_file .exists ():
323+ monitoring_file .unlink ()
324+ cleared_items .append ("monitoring token file" )
325+
326+ # Remove directory if empty
327+ try :
328+ if not any (session_dir .iterdir ()):
329+ session_dir .rmdir ()
330+ except Exception :
331+ pass
332+
333+ return cleared_items
263334
264335 def save_monitoring_token (self , id_token , token_claims ):
265336 """Save ID token for monitoring authentication"""
@@ -620,6 +691,23 @@ def get_aws_credentials(self, id_token, token_claims):
620691 return formatted_creds
621692
622693 except Exception as e :
694+ # Check if this is a credential error that suggests bad cached credentials
695+ error_str = str (e )
696+ if any (err in error_str for err in [
697+ "InvalidParameterException" ,
698+ "NotAuthorizedException" ,
699+ "ValidationError" ,
700+ "Invalid AccessKeyId" ,
701+ "Token is not from a supported provider"
702+ ]):
703+ self ._debug_print ("Detected invalid credentials, clearing cache..." )
704+ self .clear_cached_credentials ()
705+ # Add helpful message for user
706+ raise Exception (
707+ f"Authentication failed - cached credentials were invalid and have been cleared.\n "
708+ f"Please try again to re-authenticate.\n "
709+ f"Original error: { error_str } "
710+ )
623711 raise Exception (f"Failed to get AWS credentials: { str (e )} " )
624712
625713 def _wait_for_auth_completion (self , timeout = 60 ):
@@ -752,19 +840,49 @@ def main():
752840 parser .add_argument (
753841 "--get-monitoring-token" , action = "store_true" , help = "Get cached monitoring token instead of AWS credentials"
754842 )
843+ parser .add_argument (
844+ "--clear-cache" , action = "store_true" , help = "Clear cached credentials and force re-authentication"
845+ )
755846
756847 args = parser .parse_args ()
757848
758849 auth = MultiProviderAuth (profile = args .profile )
759850
851+ # Handle cache clearing request
852+ if args .clear_cache :
853+ cleared = auth .clear_cached_credentials ()
854+ if cleared :
855+ print (f"Cleared cached credentials for profile '{ args .profile } ':" , file = sys .stderr )
856+ for item in cleared :
857+ print (f" • { item } " , file = sys .stderr )
858+ else :
859+ print (f"No cached credentials found for profile '{ args .profile } '" , file = sys .stderr )
860+ sys .exit (0 )
861+
760862 # Handle monitoring token request
761863 if args .get_monitoring_token :
762864 token = auth .get_monitoring_token ()
763865 if token :
764866 print (token )
765867 sys .exit (0 )
766868 else :
767- self ._debug_print ("No valid monitoring token found. Please authenticate first." )
869+ # No cached token, trigger authentication to get one
870+ auth ._debug_print ("No valid monitoring token found, triggering authentication..." )
871+ try :
872+ # Run the normal authentication flow
873+ exit_code = auth .run ()
874+ if exit_code == 0 :
875+ # Authentication succeeded, try to get the token again
876+ token = auth .get_monitoring_token ()
877+ if token :
878+ print (token )
879+ sys .exit (0 )
880+ except Exception as e :
881+ auth ._debug_print (f"Authentication failed: { e } " )
882+
883+ # If we get here, couldn't get a token even after auth attempt
884+ # Return failure exit code so OTEL helper knows auth failed
885+ # This prevents OTEL helper from using default/unknown values
768886 sys .exit (1 )
769887
770888 # Normal AWS credential flow
0 commit comments