@@ -932,23 +932,47 @@ func isRemoteClaudeAuthenticated(sshAlias string) bool {
932932 return checkCmd .Run () == nil
933933}
934934
935- // tryTransferClaudeOAuthSession checks for a local ~/.claude/.credentials.json
936- // and offers to transfer it to the remote instance. This is a session transfer:
937- // the local file is removed after copying so the token is only active on
938- // the remote machine.
935+ // tryTransferClaudeOAuthSession checks for Claude Code OAuth credentials
936+ // locally and offers to transfer them to the remote instance. It checks:
937+ // 1. ~/.claude/.credentials.json (file on disk)
938+ // 2. macOS Keychain entry "Claude Code-credentials" (Max subscription OAuth)
939+ //
940+ // This is a session transfer: the local credentials are removed after copying
941+ // so the token is only active on the remote machine.
939942func tryTransferClaudeOAuthSession (t * terminal.Terminal , sshAlias string ) {
940943 homeDir , err := os .UserHomeDir ()
941944 if err != nil {
942945 return
943946 }
944947 localCredPath := homeDir + "/.claude/.credentials.json"
945948
946- // Check if local credentials file exists
947- if _ , err := os .Stat (localCredPath ); os .IsNotExist (err ) {
949+ // Determine credential source: file on disk or macOS Keychain
950+ hasFile := false
951+ hasKeychain := false
952+ var keychainCreds string
953+
954+ if _ , err := os .Stat (localCredPath ); err == nil {
955+ hasFile = true
956+ }
957+
958+ if runtime .GOOS == "darwin" && ! hasFile {
959+ creds , err := getClaudeCredentialsFromKeychain ()
960+ if err == nil && creds != "" {
961+ hasKeychain = true
962+ keychainCreds = creds
963+ }
964+ }
965+
966+ if ! hasFile && ! hasKeychain {
948967 return
949968 }
950969
951- t .Vprintf ("%s" , t .Yellow ("\n Found Claude Code OAuth session in ~/.claude/.credentials.json\n " ))
970+ source := "~/.claude/.credentials.json"
971+ if hasKeychain {
972+ source = "macOS Keychain (Claude Code-credentials)"
973+ }
974+
975+ t .Vprintf ("%s" , t .Yellow (fmt .Sprintf ("\n Found Claude Code OAuth session in %s\n " , source )))
952976 t .Vprintf ("%s" , t .Yellow ("Transferring this session will move your auth to the remote instance\n " ))
953977 t .Vprintf ("%s" , t .Yellow ("and log you out locally (the token can only be active in one place).\n \n " ))
954978
@@ -968,23 +992,53 @@ func tryTransferClaudeOAuthSession(t *terminal.Terminal, sshAlias string) {
968992 return
969993 }
970994
971- // SCP the credentials file to remote
972- scpCmd := exec .Command ("scp" , localCredPath , sshAlias + ":~/.claude/.credentials.json" ) // #nosec G204
973- output , err := scpCmd .CombinedOutput ()
974- if err != nil {
975- t .Vprintf (t .Red ("Failed to transfer credentials: %s\n %s\n " ), err , string (output ))
976- return
977- }
978-
979- // Remove local credentials file
980- if err := os .Remove (localCredPath ); err != nil {
981- t .Vprintf (t .Red ("Transferred to remote but failed to remove local credentials: %v\n " ), err )
982- return
995+ if hasFile {
996+ // SCP the credentials file to remote
997+ scpCmd := exec .Command ("scp" , localCredPath , sshAlias + ":~/.claude/.credentials.json" ) // #nosec G204
998+ output , err := scpCmd .CombinedOutput ()
999+ if err != nil {
1000+ t .Vprintf (t .Red ("Failed to transfer credentials: %s\n %s\n " ), err , string (output ))
1001+ return
1002+ }
1003+ // Remove local credentials file
1004+ if err := os .Remove (localCredPath ); err != nil {
1005+ t .Vprintf (t .Red ("Transferred to remote but failed to remove local credentials: %v\n " ), err )
1006+ return
1007+ }
1008+ } else {
1009+ // Write keychain credentials to remote via SSH
1010+ writeCmd := exec .Command (
1011+ "ssh" , sshAlias ,
1012+ fmt .Sprintf (`cat > "$HOME/.claude/.credentials.json" << 'BREV_EOF'
1013+ %s
1014+ BREV_EOF` , keychainCreds ),
1015+ ) // #nosec G204
1016+ output , err := writeCmd .CombinedOutput ()
1017+ if err != nil {
1018+ t .Vprintf (t .Red ("Failed to transfer credentials: %s\n %s\n " ), err , string (output ))
1019+ return
1020+ }
1021+ // Delete the keychain entry locally
1022+ deleteCmd := exec .Command ("security" , "delete-generic-password" , "-s" , "Claude Code-credentials" ) // #nosec G204
1023+ if err := deleteCmd .Run (); err != nil {
1024+ t .Vprintf (t .Red ("Transferred to remote but failed to remove local Keychain entry: %v\n " ), err )
1025+ return
1026+ }
9831027 }
9841028
9851029 t .Vprintf ("%s" , t .Green ("OAuth session transferred to remote instance. You are now logged out locally.\n " ))
9861030}
9871031
1032+ // getClaudeCredentialsFromKeychain reads the OAuth credentials stored by
1033+ // Claude Code in the macOS Keychain under "Claude Code-credentials".
1034+ func getClaudeCredentialsFromKeychain () (string , error ) {
1035+ out , err := exec .Command ("security" , "find-generic-password" , "-s" , "Claude Code-credentials" , "-w" ).Output () // #nosec G204
1036+ if err != nil {
1037+ return "" , err
1038+ }
1039+ return strings .TrimSpace (string (out )), nil
1040+ }
1041+
9881042// getClaudeKeyFromKeychain reads the API key stored by Claude Code in the
9891043// macOS Keychain (security framework).
9901044func getClaudeKeyFromKeychain () (string , error ) {
0 commit comments