Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion pkg/cmd/cmd_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ func init() {
Name: "debug",
Usage: "Print the token exchange status and Request-Id to stderr",
},
&cli.BoolFlag{
Name: "no-activate",
Usage: "Do not update active_config after login; pass --profile or ANTHROPIC_PROFILE on later commands.",
},
},
},
{
Expand Down Expand Up @@ -476,6 +480,8 @@ func authLogin(ctx context.Context, c *cli.Command) error {
}

// Decide whether this login should also become the active profile:
// - --no-activate → never activate (opt-out for external-tool bootstrap;
// active_config is shared with Claude Code / the Claude Agent SDK).
// - --profile/ANTHROPIC_PROFILE explicitly given → always activate
// (the user named a target; make subsequent commands use it).
// - profile came from active_config / "default" → only write
Expand All @@ -484,7 +490,7 @@ func authLogin(ctx context.Context, c *cli.Command) error {
if data, err := os.ReadFile(config.ActiveConfigPath(dir)); err == nil {
prevActive = strings.TrimSpace(string(data))
}
wantActivate := c.IsSet("profile") || prevActive == ""
wantActivate := !c.Bool("no-activate") && (c.IsSet("profile") || prevActive == "")
activated := false
if wantActivate && prevActive != profile {
if err := config.SetActiveProfile(dir, profile); err != nil {
Expand All @@ -508,6 +514,9 @@ func authLogin(ctx context.Context, c *cli.Command) error {
fmt.Fprintf(os.Stderr, " → set as active profile (was %q)\n", prevActive)
}
}
if !activated && c.Bool("no-activate") && prevActive != "" && prevActive != profile {
fmt.Fprintf(os.Stderr, " → active profile unchanged (still %q; --no-activate in effect)\n", prevActive)
}
fmt.Fprintf(os.Stderr, " config: %s\n credentials: %s\n",
config.ProfilePath(dir, profile), credsPath)
return nil
Expand Down
56 changes: 56 additions & 0 deletions pkg/cmd/cmd_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ func loginCmdDef() *cli.Command {
&cli.StringFlag{Name: "scope"},
&cli.StringFlag{Name: "workspace-id"},
&cli.BoolFlag{Name: "debug"},
&cli.BoolFlag{Name: "no-activate"},
},
}},
}
Expand Down Expand Up @@ -1015,6 +1016,61 @@ func TestAuthLoginActivatesExplicitProfile(t *testing.T) {
assert.Equal(t, "b", strings.TrimSpace(string(mustRead(t, config.ActiveConfigPath(dir)))))
}

// TestAuthLoginNoActivate guards the --no-activate opt-out: credentials
// must still be written, but active_config must not be created or
// retargeted regardless of whether --profile is given via flag or
// ANTHROPIC_PROFILE env source.
func TestAuthLoginNoActivate(t *testing.T) {
t.Run("retarget skipped, prior active preserved", func(t *testing.T) {
dir := t.TempDir()
t.Setenv("ANTHROPIC_CONFIG_DIR", dir)
clearEnv(t, "ANTHROPIC_PROFILE")
require.NoError(t, config.SetActiveProfile(dir, "a"))

srv := newTokenServer(t, tokenResponse{AccessToken: "tok", RefreshToken: "rt", ExpiresIn: 600})
_, stderr, err := driveLoginErr(t, srv.URL, "--profile", "b", "--no-activate")
require.NoError(t, err)

assert.Equal(t, "a", strings.TrimSpace(string(mustRead(t, config.ActiveConfigPath(dir)))),
"--no-activate must not retarget active_config")
assert.FileExists(t, config.ProfileCredentialsPath(dir, "b"),
"credentials/<profile>.json must still be written")
assert.Contains(t, stderr,
`→ active profile unchanged (still "a"; --no-activate in effect)`,
"stderr must announce the opt-out skip with the prior active value")
})

t.Run("first login leaves active_config absent (silent)", func(t *testing.T) {
dir := t.TempDir()
t.Setenv("ANTHROPIC_CONFIG_DIR", dir)
clearEnv(t, "ANTHROPIC_PROFILE")

srv := newTokenServer(t, tokenResponse{AccessToken: "tok", RefreshToken: "rt", ExpiresIn: 600})
_, stderr, err := driveLoginErr(t, srv.URL, "--profile", "c", "--no-activate")
require.NoError(t, err)

assert.NoFileExists(t, config.ActiveConfigPath(dir),
"--no-activate must not create active_config when absent")
assert.NotContains(t, stderr, "active profile unchanged",
"no opt-out skip message when there was no prior active")
})

t.Run("env-source profile honors --no-activate", func(t *testing.T) {
dir := t.TempDir()
t.Setenv("ANTHROPIC_CONFIG_DIR", dir)
require.NoError(t, config.SetActiveProfile(dir, "x"))
t.Setenv("ANTHROPIC_PROFILE", "y")

srv := newTokenServer(t, tokenResponse{AccessToken: "tok", RefreshToken: "rt", ExpiresIn: 600})
_, _, err := driveLoginErr(t, srv.URL, "--no-activate")
require.NoError(t, err)

assert.Equal(t, "x", strings.TrimSpace(string(mustRead(t, config.ActiveConfigPath(dir)))),
"--no-activate honored when profile resolves via ANTHROPIC_PROFILE")
assert.FileExists(t, config.ProfileCredentialsPath(dir, "y"))
})
}

// TestAuthLoginHonorsActiveConfig is a regression test for `auth login`
// writing to "default" instead of the active_config profile when no
// --profile/ANTHROPIC_PROFILE is set. Before the fix, the login flag had
Expand Down