From 9fd19096afd5f203191e039e45e8341b44df963d Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Fri, 1 May 2026 14:58:25 -0400 Subject: [PATCH] Stop encouraging API key on argv for auth login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chris flagged that 'quartr auth login --api-key "$KEY"' leaks the key via shell history, ps, scrollback, and CI logs — anywhere argv is visible. This is especially bad for auth login, which is the exact moment we're committing the key to disk. Changes: - Drop the duplicate subcommand-local --api-key flag from auth login. It was always redundant with the global --api-key (which is stripped in extractGlobalFlags before subcommands ever see it), so removing it causes no behavior change for any reasonable invocation. - Add --api-key-stdin to auth login: reads one trimmed line from stdin. Standard pattern (gh, kubectl, op) for piping from a secret store. - Update Makefile install target to pipe via stdin instead of argv. - Update README and SKILL.md to recommend 'export QUARTR_API_KEY=...; quartr auth login' (uses env via the standard config precedence) or '... | quartr auth login --api-key-stdin' for piped input. The global --api-key flag is preserved for one-off commands where the trade-off is knowingly accepted (CI scripts, throwaway containers). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/quartr/SKILL.md | 13 +++++++++++-- Makefile | 2 +- README.md | 14 +++++++++++++- internal/cli/handlers.go | 17 ++++++++++++++--- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/.claude/skills/quartr/SKILL.md b/.claude/skills/quartr/SKILL.md index 1b5260a..d052ffd 100644 --- a/.claude/skills/quartr/SKILL.md +++ b/.claude/skills/quartr/SKILL.md @@ -29,10 +29,19 @@ Precedence (highest to lowest): 2. `QUARTR_API_KEY` env var 3. `~/.config/quartr/config.json` (written 0600 by `quartr auth login`) -For durable use: +For durable use, run `quartr auth login` once with `QUARTR_API_KEY` exported — +the command picks up the env var via the standard precedence and persists it +to `~/.config/quartr/config.json`. Avoid `--api-key VALUE` on `auth login`: +the literal key ends up in shell history, `ps`, scrollback, and CI logs. +When piping from a secret store, use `--api-key-stdin`: ```bash -quartr auth login --api-key "$QUARTR_API_KEY" +# Idiomatic +export QUARTR_API_KEY=... +quartr auth login + +# Pipe from a secret store +op read op://Personal/Quartr/api_key | quartr auth login --api-key-stdin ``` For project-scoped use with a `.env` file: diff --git a/Makefile b/Makefile index 60c53a6..e4028dd 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ build: install: go install ./cmd/quartr - @if [ -n "$$QUARTR_API_KEY" ]; then $(GOBIN)/quartr auth login --api-key "$$QUARTR_API_KEY"; fi + @if [ -n "$$QUARTR_API_KEY" ]; then printf '%s' "$$QUARTR_API_KEY" | $(GOBIN)/quartr auth login --api-key-stdin; fi test: go test ./... diff --git a/README.md b/README.md index 4e4fcbb..ff6b9d0 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,22 @@ export QUARTR_API_KEY="your-api-key" Or store it locally: ```bash -quartr auth login --api-key "$QUARTR_API_KEY" +# Reads QUARTR_API_KEY from the environment if set; otherwise prompts. +quartr auth login quartr auth show ``` +The key is written to `~/.config/quartr/config.json` with file mode `0600`. + +For piping the key in (e.g. from a secret store) without exposing it via argv: + +```bash +op read op://Personal/Quartr/api_key | quartr auth login --api-key-stdin +``` + +`--api-key VALUE` is also supported but discouraged for `auth login` because +the value leaks via shell history, `ps`, terminal scrollback, and CI logs. + Config precedence is: 1. global CLI flags diff --git a/internal/cli/handlers.go b/internal/cli/handlers.go index bf68a16..558bb6b 100644 --- a/internal/cli/handlers.go +++ b/internal/cli/handlers.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "io" "net/url" "os" "path/filepath" @@ -23,7 +24,7 @@ func (a *app) handleAuth(args []string) error { switch args[0] { case "login", "set-key": fs := newFlagSet("auth login", a.errOut) - apiKey := fs.String("api-key", "", "Quartr API key") + apiKeyStdin := fs.Bool("api-key-stdin", false, "read API key from stdin (one line, trimmed)") baseURL := fs.String("base-url", a.cfg.BaseURL(), "API base URL") format := fs.String("format", a.cfg.Format(), "default output format") timeout := fs.String("timeout", a.cfg.Timeout(), "default HTTP timeout, e.g. 30s") @@ -31,8 +32,18 @@ func (a *app) handleAuth(args []string) error { return err } - key := strings.TrimSpace(*apiKey) - if key == "" { + // Source priority: --api-key-stdin > resolved config (global flag/env/file) > interactive prompt. + // The global --api-key flag is honored via a.cfg.APIKey() but discouraged for `auth login` + // because the literal value leaks via argv (ps, history, scrollback, CI logs). + var key string + switch { + case *apiKeyStdin: + line, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil && !errors.Is(err, io.EOF) { + return fmt.Errorf("read api key from stdin: %w", err) + } + key = strings.TrimSpace(line) + default: key = strings.TrimSpace(a.cfg.APIKey()) } if key == "" {