From 5f7715fdfa4cdd5bff0963fa81b3aee57b248441 Mon Sep 17 00:00:00 2001 From: tak848 Date: Thu, 7 May 2026 07:49:37 +0900 Subject: [PATCH 1/2] feat(cli): add --no-set-active flag to auth login Allow `ant auth login` to skip writing the logged-in profile to active_config. Default behavior is unchanged. Use case: bootstrapping a profile for an external tool without retargeting the active profile used by Claude Code and Claude Agent SDK consumers, which share the same active_config resolution per the WIF reference. Covered by an end-to-end test (TestAuthLoginNoSetActive) that exercises the active_config write path against the existing token-server harness, including the ANTHROPIC_PROFILE env-source path. No SDK-side change is required. --- pkg/cmd/cmd_auth.go | 11 +++++++- pkg/cmd/cmd_auth_test.go | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/cmd_auth.go b/pkg/cmd/cmd_auth.go index dc2ced0..42fd6df 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-set-active", + 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-set-active → 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-set-active") && (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-set-active") && prevActive != "" && prevActive != profile { + fmt.Fprintf(os.Stderr, " → active profile unchanged (still %q; --no-set-active 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..2c47f8d 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-set-active"}, }, }}, } @@ -1015,6 +1016,61 @@ func TestAuthLoginActivatesExplicitProfile(t *testing.T) { assert.Equal(t, "b", strings.TrimSpace(string(mustRead(t, config.ActiveConfigPath(dir))))) } +// TestAuthLoginNoSetActive guards the --no-set-active 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 TestAuthLoginNoSetActive(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-set-active") + require.NoError(t, err) + + assert.Equal(t, "a", strings.TrimSpace(string(mustRead(t, config.ActiveConfigPath(dir)))), + "--no-set-active 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-set-active 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-set-active") + require.NoError(t, err) + + assert.NoFileExists(t, config.ActiveConfigPath(dir), + "--no-set-active 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-set-active", 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-set-active") + require.NoError(t, err) + + assert.Equal(t, "x", strings.TrimSpace(string(mustRead(t, config.ActiveConfigPath(dir)))), + "--no-set-active 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 From 1ff3b2ab0ae93f6f924574c5e443643b71cab53d Mon Sep 17 00:00:00 2001 From: tak848 Date: Thu, 7 May 2026 08:55:07 +0900 Subject: [PATCH 2/2] refactor(cli): rename --no-set-active to --no-activate Match `ant`'s existing `profile activate` verb and the `--no-` flag pattern used elsewhere in this CLI (e.g. `--no-browser`). The previous name leaked the internal `active_config` filename into the public flag surface; the operation users already think in is "activate". No behavior change. Renames the flag, the corresponding stderr message, and TestAuthLoginNoActivate. --- pkg/cmd/cmd_auth.go | 10 +++++----- pkg/cmd/cmd_auth_test.go | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/cmd_auth.go b/pkg/cmd/cmd_auth.go index 42fd6df..7f2a75c 100644 --- a/pkg/cmd/cmd_auth.go +++ b/pkg/cmd/cmd_auth.go @@ -134,7 +134,7 @@ func init() { Usage: "Print the token exchange status and Request-Id to stderr", }, &cli.BoolFlag{ - Name: "no-set-active", + Name: "no-activate", Usage: "Do not update active_config after login; pass --profile or ANTHROPIC_PROFILE on later commands.", }, }, @@ -480,7 +480,7 @@ func authLogin(ctx context.Context, c *cli.Command) error { } // Decide whether this login should also become the active profile: - // - --no-set-active → never activate (opt-out for external-tool bootstrap; + // - --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). @@ -490,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.Bool("no-set-active") && (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 { @@ -514,8 +514,8 @@ 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-set-active") && prevActive != "" && prevActive != profile { - fmt.Fprintf(os.Stderr, " → active profile unchanged (still %q; --no-set-active in effect)\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) diff --git a/pkg/cmd/cmd_auth_test.go b/pkg/cmd/cmd_auth_test.go index 2c47f8d..5b1a1b5 100644 --- a/pkg/cmd/cmd_auth_test.go +++ b/pkg/cmd/cmd_auth_test.go @@ -658,7 +658,7 @@ func loginCmdDef() *cli.Command { &cli.StringFlag{Name: "scope"}, &cli.StringFlag{Name: "workspace-id"}, &cli.BoolFlag{Name: "debug"}, - &cli.BoolFlag{Name: "no-set-active"}, + &cli.BoolFlag{Name: "no-activate"}, }, }}, } @@ -1016,11 +1016,11 @@ func TestAuthLoginActivatesExplicitProfile(t *testing.T) { assert.Equal(t, "b", strings.TrimSpace(string(mustRead(t, config.ActiveConfigPath(dir))))) } -// TestAuthLoginNoSetActive guards the --no-set-active opt-out: credentials +// 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 TestAuthLoginNoSetActive(t *testing.T) { +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) @@ -1028,15 +1028,15 @@ func TestAuthLoginNoSetActive(t *testing.T) { 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-set-active") + _, 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-set-active must not retarget active_config") + "--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-set-active in effect)`, + `→ active profile unchanged (still "a"; --no-activate in effect)`, "stderr must announce the opt-out skip with the prior active value") }) @@ -1046,27 +1046,27 @@ func TestAuthLoginNoSetActive(t *testing.T) { clearEnv(t, "ANTHROPIC_PROFILE") srv := newTokenServer(t, tokenResponse{AccessToken: "tok", RefreshToken: "rt", ExpiresIn: 600}) - _, stderr, err := driveLoginErr(t, srv.URL, "--profile", "c", "--no-set-active") + _, stderr, err := driveLoginErr(t, srv.URL, "--profile", "c", "--no-activate") require.NoError(t, err) assert.NoFileExists(t, config.ActiveConfigPath(dir), - "--no-set-active must not create active_config when absent") + "--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-set-active", func(t *testing.T) { + 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-set-active") + _, _, err := driveLoginErr(t, srv.URL, "--no-activate") require.NoError(t, err) assert.Equal(t, "x", strings.TrimSpace(string(mustRead(t, config.ActiveConfigPath(dir)))), - "--no-set-active honored when profile resolves via ANTHROPIC_PROFILE") + "--no-activate honored when profile resolves via ANTHROPIC_PROFILE") assert.FileExists(t, config.ProfileCredentialsPath(dir, "y")) }) }