diff --git a/pkg/cmd/cmd_auth.go b/pkg/cmd/cmd_auth.go index dc2ced0..7f2a75c 100644 --- a/pkg/cmd/cmd_auth.go +++ b/pkg/cmd/cmd_auth.go @@ -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.", + }, }, }, { @@ -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 @@ -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 { @@ -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 diff --git a/pkg/cmd/cmd_auth_test.go b/pkg/cmd/cmd_auth_test.go index 5fce12b..5b1a1b5 100644 --- a/pkg/cmd/cmd_auth_test.go +++ b/pkg/cmd/cmd_auth_test.go @@ -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"}, }, }}, } @@ -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/.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