From af3051a2340ca729ba65d03453aeae0807f4b291 Mon Sep 17 00:00:00 2001 From: Daniel Spangenberger Date: Thu, 25 Jun 2026 14:07:43 -0400 Subject: [PATCH] fix(config): remove --api-key flag from `omni config init` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #63 added an --api-key flag for non-interactive setup. Accepting a secret on the command line leaks it into shell history and process listings (and from there into anything scraping them, including agentic tooling). Drop the flag and always read the key from a hidden prompt. The non-secret --name, --endpoint, and --auth flags remain, so the primary motivation — one-command non-interactive OAuth setup — still works. For api-key auth the key is now always read via term.ReadPassword, even when --auth api-key is supplied non-interactively. Removes the two tests that exercised the deleted flag. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EfZEjYSR3cRVTHgifwJYjK --- cmd/omni/config_commands.go | 34 +++++++--------- cmd/omni/config_commands_test.go | 68 +++----------------------------- 2 files changed, 20 insertions(+), 82 deletions(-) diff --git a/cmd/omni/config_commands.go b/cmd/omni/config_commands.go index 828bf21..c54f7d7 100644 --- a/cmd/omni/config_commands.go +++ b/cmd/omni/config_commands.go @@ -48,7 +48,6 @@ func configInitCmd() *cobra.Command { name string endpoint string authMethod string - apiKey string ) cmd := &cobra.Command{ @@ -57,15 +56,17 @@ func configInitCmd() *cobra.Command { Long: `Create a new configuration profile. Prompts interactively for any value not supplied via flags. With --name, ---endpoint, and --auth all set, no prompts are shown.`, +--endpoint, and --auth oauth set, OAuth setup runs with no prompts. For +api-key auth the key is always read from a hidden prompt — it is never +accepted as a flag, so it can't leak into shell history.`, Example: ` # Interactive setup omni config init # Non-interactive OAuth (opens browser for login) omni config init --name prod --endpoint https://myorg.omniapp.co --auth oauth - # Non-interactive API key - omni config init --name prod --endpoint https://myorg.omniapp.co --api-key "$OMNI_API_TOKEN"`, + # API key (prompts securely for the key) + omni config init --name prod --endpoint https://myorg.omniapp.co --auth api-key`, RunE: func(cmd *cobra.Command, args []string) error { reader := bufio.NewReader(os.Stdin) @@ -86,8 +87,6 @@ Prompts interactively for any value not supplied via flags. With --name, choice := strings.TrimSpace(strings.ToLower(authMethod)) switch { - case choice == "" && apiKey != "": - choice = "api-key" case choice == "": fmt.Println("Authentication method:") fmt.Println(" 1) API key") @@ -99,10 +98,6 @@ Prompts interactively for any value not supplied via flags. With --name, return fmt.Errorf("invalid --auth %q — must be %q or %q", authMethod, "api-key", "oauth") } - if choice == "oauth" && apiKey != "" { - return fmt.Errorf("--api-key cannot be combined with --auth oauth") - } - cfg, err := config.Load() if err != nil { cfg = &config.Config{ @@ -126,15 +121,15 @@ Prompts interactively for any value not supplied via flags. With --name, cfg.Profiles[name] = p default: // "1", "a", "api-key", or empty - if apiKey == "" { - fmt.Print("API key: ") - apiKeyBytes, err := term.ReadPassword(int(os.Stdin.Fd())) - fmt.Println() - if err != nil { - return fmt.Errorf("reading API key: %w", err) - } - apiKey = strings.TrimSpace(string(apiKeyBytes)) + // The key is always read from a hidden prompt rather than a + // flag, so it never lands in shell history or process listings. + fmt.Print("API key: ") + apiKeyBytes, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + return fmt.Errorf("reading API key: %w", err) } + apiKey := strings.TrimSpace(string(apiKeyBytes)) cfg.Profiles[name] = config.Profile{ APIEndpoint: endpoint, @@ -158,8 +153,7 @@ Prompts interactively for any value not supplied via flags. With --name, cmd.Flags().StringVar(&name, "name", "", "profile name (skips prompt)") cmd.Flags().StringVar(&endpoint, "endpoint", "", "API endpoint, e.g. https://myorg.omniapp.co (skips prompt)") - cmd.Flags().StringVar(&authMethod, "auth", "", `authentication method: "api-key" or "oauth" (skips prompt)`) - cmd.Flags().StringVar(&apiKey, "api-key", "", "API key (skips prompt; implies --auth api-key)") + cmd.Flags().StringVar(&authMethod, "auth", "", `authentication method: "api-key" or "oauth" (skips prompt). The API key itself is always read from a hidden prompt, never a flag.`) return cmd } diff --git a/cmd/omni/config_commands_test.go b/cmd/omni/config_commands_test.go index 3c8da59..4ccbc58 100644 --- a/cmd/omni/config_commands_test.go +++ b/cmd/omni/config_commands_test.go @@ -82,47 +82,12 @@ func TestApplyOAuthToken_CopiesAllFields(t *testing.T) { } // --- config init (non-interactive flags) --- - -// With --name, --endpoint, and --api-key all supplied, init must not prompt -// for anything and should write the profile straight to disk. -func TestConfigInit_NonInteractiveAPIKey(t *testing.T) { - withConfig(t, nil) - - cmd := configInitCmd() - cmd.SetArgs([]string{ - "--name", "prod", - "--endpoint", "https://myorg.omniapp.co", - "--api-key", "sk-test-1234", - }) - out := captureStdout(t, func() { - if err := cmd.Execute(); err != nil { - t.Fatalf("Execute: %v", err) - } - }) - if strings.Contains(out, "Profile name:") || strings.Contains(out, "Choose [1/2]:") { - t.Errorf("expected no interactive prompts, got:\n%s", out) - } - - cfg, err := config.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - p := cfg.Profiles["prod"] - if p.APIEndpoint != "https://myorg.omniapp.co" { - t.Errorf("APIEndpoint = %q", p.APIEndpoint) - } - // --api-key alone implies --auth api-key. - if p.AuthMethod != "api-key" { - t.Errorf("AuthMethod = %q, want %q", p.AuthMethod, "api-key") - } - if p.APIKey != "sk-test-1234" { - t.Errorf("APIKey = %q", p.APIKey) - } - // First profile becomes the default. - if cfg.DefaultProfile != "prod" { - t.Errorf("DefaultProfile = %q, want %q", cfg.DefaultProfile, "prod") - } -} +// +// Note: there is deliberately no --api-key flag. Accepting a secret on the +// command line leaks it into shell history and process listings (and from +// there into anything that scrapes those, including agentic tooling). The key +// is always read from a hidden prompt instead; only the non-secret --name, +// --endpoint, and --auth values can be supplied non-interactively. // --auth oauth must validate the endpoint BEFORE launching the browser flow, // so a bad endpoint fails fast instead of opening a browser against it. @@ -167,27 +132,6 @@ func TestConfigInit_InvalidAuthFlag(t *testing.T) { } } -func TestConfigInit_APIKeyConflictsWithOAuth(t *testing.T) { - withConfig(t, nil) - - cmd := configInitCmd() - cmd.SilenceUsage = true - cmd.SilenceErrors = true - cmd.SetArgs([]string{ - "--name", "prod", - "--endpoint", "https://myorg.omniapp.co", - "--auth", "oauth", - "--api-key", "sk-test-1234", - }) - err := cmd.Execute() - if err == nil { - t.Fatal("expected conflict error, got nil") - } - if !strings.Contains(err.Error(), "--api-key cannot be combined") { - t.Errorf("error = %q, want api-key/oauth conflict message", err.Error()) - } -} - // --- config logout --- func TestConfigLogout_ClearsTokensForNamedProfile(t *testing.T) {