package main
// Where the credential comes from.
//
// An API key or a gateway token is the ordinary answer, and nothing here
// touches one: the llm package reads them from the flags and the environment.
// What this file decides is the other answer — the OAuth login Claude Code
// keeps in ~/.claude/.credentials.json — and the rule for it is that it is
// borrowed only when someone asked. That login is issued to Claude Code, and
// using it from another client is outside Anthropic's terms for it. It used to
// be borrowed silently whenever nothing else was set; that was tolerable on
// the one machine Codey was written on and is not for a binary other people
// install, where "a file happened to exist" is not consent.
import (
"bufio"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"github.com/johannSo/codey/internal/claudecode"
"github.com/johannSo/codey/internal/config"
"github.com/johannSo/codey/internal/llm"
)
// claudeCodeAllowed is whether this run may borrow Claude Code's login, read
// in order of how deliberate each source is: -no-claude-code-auth, then
// -claude-code-auth, then the config, then off.
//
// The refusal outranks the request so that a launch line which has collected
// both — a wrapper script that adds one, a settings box that adds the other —
// comes out on the side that sends nothing it was not sure about. And the
// default is off, so that only a person can turn it on.
func claudeCodeAllowed(refuse, request bool, cfg config.Config) bool {
switch {
case refuse:
return false
case request:
return true
}
return cfg.ClaudeCodeAuthEnabled()
}
// claudeCodeAskable reports whether the question of borrowing Claude Code's
// login arises at all for this client: nothing else was supplied, the
// endpoint is Anthropic's, and there is a login on disk to borrow. Anywhere
// else the answer would change nothing, and a question whose answer changes
// nothing is one the wizard should not ask and an error should not suggest.
//
// It looks for the file without reading it. Whether what is in there is a
// usable token is a question for after someone has said yes.
func claudeCodeAskable(client *llm.Client, home string) bool {
return !client.HasCredentials() && client.IsDefaultEndpoint() && claudecode.Present(home)
}
// resolveClaudeCode returns a token source borrowed from Claude Code's login,
// or nil when one should not be used. The second return is a line for the
// user: a borrowed credential is worth announcing, the same way a non-default
// endpoint is.
//
// Three conditions gate it. It has to have been asked for, which is what
// allowed carries — see claudeCodeAllowed. The client must have no credential
// already, because we never override what the user typed. And the endpoint
// must be Anthropic's, because a credential we found on disk is not one the
// user chose to send — forwarding it to a -base-url someone else operates
// would leak it to a third party because a file happened to exist. All three
// are checked before the file is opened.
func resolveClaudeCode(client *llm.Client, home string, allowed bool) (*claudecode.Source, string, error) {
if !allowed || client.HasCredentials() || !client.IsDefaultEndpoint() {
return nil, "", nil
}
creds, err := claudecode.Load(home)
if errors.Is(err, claudecode.ErrNotFound) {
return nil, "", nil
}
if err != nil {
return nil, "", err
}
note := fmt.Sprintf("using your Claude Code login (%s plan) from ~/%s", creds.SubscriptionType, claudecode.Filename)
// home is handed on so a rotated refresh token can be written back. That
// is the only circumstance in which this process writes to Claude Code's
// file, and not writing then is what would break its login.
//
// The refresh client is deliberately not Codey's: that one allows
// ten minutes for a model to answer, which is the wrong budget for a token
// exchange running under a lock. NewSource supplies a short-timeout client.
return claudecode.NewSource(creds, nil, home), note, nil
}
// borrowed is the client startup ends up with, and what it took to get it.
type borrowed struct {
client *llm.Client
// opts is the list the client was built from, kept so a second client on
// another model can be built the same way.
opts []llm.Option
// source is set only when the credential is Claude Code's, so its
// write-back failure hook can be wired once there is somewhere to report.
source *claudecode.Source
note string
}
// borrowClaudeCode builds the client from opts, and rebuilds it on Claude
// Code's login when that is allowed and nothing else supplied a credential.
//
// The options are a slice rather than a client because the rebuild has to
// replay them: replaying the same list with one more option is how the rebuilt
// client is guaranteed to be the one that passed the endpoint check. It is
// the one place both the session and the `codey models` probe get their
// client from, so the probe cannot report a login the session would refuse.
func borrowClaudeCode(opts []llm.Option, home string, allowed bool) (borrowed, error) {
b := borrowed{client: llm.New(opts...), opts: opts}
if home == "" {
// No home directory is not fatal: an explicit credential still works.
return b, nil
}
src, note, err := resolveClaudeCode(b.client, home, allowed)
if err != nil || src == nil {
return b, err
}
b.opts = append(append([]llm.Option(nil), opts...), llm.WithTokenSource(src.Token))
b.client = llm.New(b.opts...)
// Not expected to ever trigger: resolveClaudeCode already checked the
// endpoint before we agreed to borrow this credential, but that check ran
// against the client we had before this rebuild. This is a guard against
// a future refactor reordering things so the two clients quietly diverge,
// not a condition anyone should see fire.
if !b.client.IsDefaultEndpoint() {
return borrowed{}, errors.New("internal: discovered credential on a non-default endpoint")
}
b.source, b.note = src, note
return b, nil
}
// shouldAskClaudeCode is whether startup, having found no credential, stops to
// ask once about the Claude Code login instead of only failing.
//
// It exists for the people who relied on the silent fallback: after upgrading,
// their next start would otherwise end in "no credentials" for no reason they
// can see. So it asks — but only of a person at a terminal, since a -p run, a
// pipe and a protocol client have nobody to answer; only when the config has
// never recorded an answer, so it is asked once rather than on every start;
// only when the config was read, since the answer is saved over it; and never
// when -no-claude-code-auth already said no.
func shouldAskClaudeCode(cfg config.Config, cfgErr error, askable, refused, tty bool) bool {
return askable && tty && !refused && cfgErr == nil && cfg.ClaudeCodeAuth == ""
}
// askClaudeCode is that question, on the plain terminal before anything takes
// the screen over. The answer is saved either way: yes so the next start does
// not ask again, and no for the same reason — someone who said no should be
// told how to get a key, not asked the same thing every morning. Anything but
// a yes is a no, an empty line included, because the cautious answer is the
// one enter should give.
func askClaudeCode(in io.Reader, out io.Writer, cfg config.Config, save func(config.Config) error) (bool, config.Config) {
fmt.Fprintf(out, "codey: no API key is set, but Claude Code is logged in on this machine (~/%s).\n", claudecode.Filename)
fmt.Fprintln(out, "codey: that login is issued to Claude Code; using it from another client is outside Anthropic's terms for it.")
fmt.Fprint(out, "codey: use it for Codey on this machine? [y/N] ")
line, _ := bufio.NewReader(in).ReadString('\n')
yes := false
switch strings.ToLower(strings.TrimSpace(line)) {
case "y", "yes":
yes = true
}
cfg.ClaudeCodeAuth = config.ClaudeCodeAuthOff
if yes {
cfg.ClaudeCodeAuth = config.ClaudeCodeAuthOn
}
if save != nil {
if err := save(cfg); err != nil {
// The answer still holds for this run; only remembering it failed,
// and the next start will ask again, which is the honest outcome.
fmt.Fprintf(out, "codey: could not save the answer: %v\n", err)
}
}
return yes, cfg
}
// apiKeysURL is where an Anthropic API key is created.
const apiKeysURL = "https://console.anthropic.com/settings/keys"
// noCredentialsError says how to get a credential, because "unauthorized"
// against a gateway that ignores the header is a confusing failure and "no
// credentials" on its own is a dead end.
//
// The Claude Code login is mentioned only where it could be borrowed — there
// is a login on disk and the endpoint is Anthropic's — and with the terms
// attached, because it is a way to start that is yours to choose on your own
// machine rather than the first thing to try.
func noCredentialsError(claudeCode bool, cfgPath string) error {
var b strings.Builder
b.WriteString("no credentials. Set one of:\n")
b.WriteString(" export ANTHROPIC_API_KEY=sk-ant-... create one at " + apiKeysURL + "\n")
b.WriteString(" -base-url URL with ANTHROPIC_AUTH_TOKEN for a gateway (or -api-key / -auth-token)")
if claudeCode {
b.WriteString("\nClaude Code is logged in here: -claude-code-auth, or \"claude_code_auth\": \"on\" in " + cfgPath + ", borrows that login.")
b.WriteString("\nIt is issued to Claude Code; using it from another client is outside Anthropic's terms for it.")
}
return errors.New(b.String())
}
// configPathLabel is where the config lives, with the home directory spelled
// as ~ so the message reads the way the README does.
func configPathLabel(home string) string {
p, err := config.Path()
if err != nil {
return "Codey's config"
}
if home != "" {
if rel, err := filepath.Rel(home, p); err == nil && !strings.HasPrefix(rel, "..") {
return "~/" + filepath.ToSlash(rel)
}
}
return p
}