Skip to content

Commit ee88bdd

Browse files
committed
feat: add Claude Code OAuth session transfer from local to remote
1 parent 782baf9 commit ee88bdd

1 file changed

Lines changed: 58 additions & 1 deletion

File tree

pkg/cmd/open/open.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -856,8 +856,12 @@ func openClaude(t *terminal.Terminal, sshAlias string, path string, claudeArgs [
856856
return breverrors.WrapAndTrace(err)
857857
}
858858

859-
// Auto-authenticate: only forward a key if the remote is not already logged in
859+
// Auto-authenticate: try API key first, then OAuth token transfer
860860
apiKey := resolveClaudeAPIKey(t, sshAlias)
861+
if apiKey == "" {
862+
// No API key found; try transferring OAuth session from local credentials
863+
tryTransferClaudeOAuthSession(t, sshAlias)
864+
}
861865

862866
sessionName := "claude"
863867

@@ -928,6 +932,59 @@ func isRemoteClaudeAuthenticated(sshAlias string) bool {
928932
return checkCmd.Run() == nil
929933
}
930934

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.
939+
func tryTransferClaudeOAuthSession(t *terminal.Terminal, sshAlias string) {
940+
homeDir, err := os.UserHomeDir()
941+
if err != nil {
942+
return
943+
}
944+
localCredPath := homeDir + "/.claude/.credentials.json"
945+
946+
// Check if local credentials file exists
947+
if _, err := os.Stat(localCredPath); os.IsNotExist(err) {
948+
return
949+
}
950+
951+
t.Vprintf("%s", t.Yellow("\nFound Claude Code OAuth session in ~/.claude/.credentials.json\n"))
952+
t.Vprintf("%s", t.Yellow("Transferring this session will move your auth to the remote instance\n"))
953+
t.Vprintf("%s", t.Yellow("and log you out locally (the token can only be active in one place).\n\n"))
954+
955+
result := terminal.PromptSelectInput(terminal.PromptSelectContent{
956+
Label: "Transfer your Claude Code OAuth session to the remote instance?",
957+
Items: []string{"Yes, transfer and log out locally", "No, skip"},
958+
})
959+
960+
if result != "Yes, transfer and log out locally" {
961+
return
962+
}
963+
964+
// Ensure remote ~/.claude directory exists
965+
mkdirCmd := exec.Command("ssh", sshAlias, `mkdir -p "$HOME/.claude"`) // #nosec G204
966+
if err := mkdirCmd.Run(); err != nil {
967+
t.Vprintf(t.Red("Failed to create remote ~/.claude directory: %v\n"), err)
968+
return
969+
}
970+
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
983+
}
984+
985+
t.Vprintf("%s", t.Green("OAuth session transferred to remote instance. You are now logged out locally.\n"))
986+
}
987+
931988
// getClaudeKeyFromKeychain reads the API key stored by Claude Code in the
932989
// macOS Keychain (security framework).
933990
func getClaudeKeyFromKeychain() (string, error) {

0 commit comments

Comments
 (0)