From d86dca00c921ea71b381624192dce536e368e0a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 04:22:15 +0000 Subject: [PATCH 1/3] fix(cli): suppress Claude Code plugin hint when stdout is non-tty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hint is already written to stderr, but redirecting stdout to a file (e.g. `supabase gen types ... > database.types.ts`) is the only scenario where this trailer matters — in interactive terminals it is either visible only to the user or stripped by Claude Code. Skipping emission when stdout is not a TTY guarantees that captured/redirected output cannot be contaminated, regardless of which stream the hint ends up on, and keeps the install-prompt UX intact for interactive shells where it is actually useful. Fixes #5212 --- apps/cli-go/internal/utils/misc.go | 14 +++++++++++--- apps/cli-go/internal/utils/misc_test.go | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/cli-go/internal/utils/misc.go b/apps/cli-go/internal/utils/misc.go index 7ba482d96..6e164acd9 100644 --- a/apps/cli-go/internal/utils/misc.go +++ b/apps/cli-go/internal/utils/misc.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/viper" "github.com/supabase/cli/internal/utils/agent" "github.com/supabase/cli/pkg/migration" + "golang.org/x/term" ) // Assigned using `-ldflags` https://stackoverflow.com/q/11354518 @@ -42,10 +43,17 @@ const SuggestDebugFlag = "Try rerunning the command with --debug to troubleshoot const claudeCodeHint = `` func SuggestClaudePlugin() string { - if agent.IsClaudeCode() { - return claudeCodeHint + if !agent.IsClaudeCode() { + return "" } - return "" + // Suppress the hint when stdout is non-interactive (redirected to a file + // or piped). Without this guard, captured output could be contaminated by + // the trailer if anything ever leaks the tag onto stdout, and the + // install-prompt UX is only meaningful in an interactive shell anyway. + if !term.IsTerminal(int(os.Stdout.Fd())) { //nolint:gosec // G115: stdout fd is a small int on supported platforms + return "" + } + return claudeCodeHint } var ( diff --git a/apps/cli-go/internal/utils/misc_test.go b/apps/cli-go/internal/utils/misc_test.go index e8e075201..8dd549d9a 100644 --- a/apps/cli-go/internal/utils/misc_test.go +++ b/apps/cli-go/internal/utils/misc_test.go @@ -215,3 +215,22 @@ func TestGetDeclarativeDir(t *testing.T) { assert.Equal(t, DeclarativeDir, GetDeclarativeDir()) }) } + +func TestSuggestClaudePlugin(t *testing.T) { + // In `go test`, os.Stdout is a pipe (not a TTY), so this exercises the + // non-interactive guard that prevents the hint from ever reaching + // captured/redirected output. + t.Run("suppresses hint when stdout is non-tty", func(t *testing.T) { + t.Setenv("CLAUDECODE", "1") + t.Setenv("CLAUDE_CODE", "") + + assert.Equal(t, "", SuggestClaudePlugin()) + }) + + t.Run("returns empty string when not running in claude code", func(t *testing.T) { + t.Setenv("CLAUDECODE", "") + t.Setenv("CLAUDE_CODE", "") + + assert.Equal(t, "", SuggestClaudePlugin()) + }) +} From 9ed32fb27bc358087efccdf4fe5b9ed7bb4c6405 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 11 May 2026 09:47:14 +0200 Subject: [PATCH 2/3] fix(cli): update `run` method to accept execution options and suppress Claude Code hint in output The `run` method in the test context now accepts an optional second argument for execution options, allowing for additional environment variables to be passed. This change ensures that when `CLAUDECODE=1` is set, the output does not include the Claude Code plugin hint, preventing issues with redirected output. This addresses the need for cleaner output when generating TypeScript types from projects. Fixes #5212 --- apps/cli-e2e/src/tests/gen.e2e.test.ts | 8 +++++++- apps/cli-e2e/src/tests/test-context.ts | 9 ++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/cli-e2e/src/tests/gen.e2e.test.ts b/apps/cli-e2e/src/tests/gen.e2e.test.ts index 3199feac2..cdb173382 100644 --- a/apps/cli-e2e/src/tests/gen.e2e.test.ts +++ b/apps/cli-e2e/src/tests/gen.e2e.test.ts @@ -12,10 +12,16 @@ describe("gen", () => { testBehaviour.skipIf(isRecording)( "generates typescript types from project", async ({ run, projectRef }) => { - const result = await run(["gen", "types", "--project-id", projectRef]); + // #5212 — with CLAUDECODE=1, piped/redirected output must not include the + // plugin hint (would break `gen types > file.ts` and similar captures). + const result = await run(["gen", "types", "--project-id", projectRef], { + env: { CLAUDECODE: "1" }, + }); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("export type Json"); expect(result.stdout).toContain("export type Database"); + expect(result.stdout).not.toMatch(/claude-code-hint/); + expect(result.stderr).not.toMatch(/claude-code-hint/); }, ); diff --git a/apps/cli-e2e/src/tests/test-context.ts b/apps/cli-e2e/src/tests/test-context.ts index 63036b1ef..613360f34 100644 --- a/apps/cli-e2e/src/tests/test-context.ts +++ b/apps/cli-e2e/src/tests/test-context.ts @@ -21,11 +21,13 @@ function scenarioSlug(task: { name: string; suite?: { name: string } | null }): return prefix + slugify(task.name); } +type ExecOptions = NonNullable[2]>; + interface BehaviourFixtures { projectRef: string; orgId: string; workspace: TempDir; - run: (cmd: string[]) => Promise; + run: (cmd: string[], execOpts?: ExecOptions) => Promise; runNoProjectId: (cmd: string[]) => Promise; apiUrl: string; storageBucket: string; @@ -38,7 +40,8 @@ interface BehaviourFixtures { * - `projectRef` — a real project ref (record mode) or the replay default * - `orgId` — a real org slug (record mode) or the replay default * - `workspace` — fresh temp dir, auto-disposed after the test - * - `run` — pre-configured `exec()` for the current TARGET + * - `run` — pre-configured `exec()` for the current TARGET (optional second + * argument forwarded as `exec` options, e.g. extra `env` entries) * - `apiUrl` — the replay server base URL (for setting up error overrides) * * Auto-wires a named scenario for the test before running it, so the replay @@ -90,7 +93,7 @@ export const testBehaviour = test.extend({ cwd: workspace.path, projectId: inject("projectRef") as string, }); - await use((cmd) => exec(harness, cmd)); + await use((cmd, execOpts) => exec(harness, cmd, execOpts)); }, runNoProjectId: async ({ workspace }, use) => { From 250d42fdb7c21f2afab3e67e749e4ed7a6aabe80 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 11 May 2026 09:48:04 +0200 Subject: [PATCH 3/3] refactor(cli): remove deprecated TestSuggestClaudePlugin from misc_test.go The TestSuggestClaudePlugin function has been removed from the misc_test.go file as it is no longer needed (covered by e2e test). This cleanup helps streamline the test suite and maintain focus on relevant tests. No changes to functionality were made. --- apps/cli-go/internal/utils/misc_test.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/apps/cli-go/internal/utils/misc_test.go b/apps/cli-go/internal/utils/misc_test.go index 8dd549d9a..e8e075201 100644 --- a/apps/cli-go/internal/utils/misc_test.go +++ b/apps/cli-go/internal/utils/misc_test.go @@ -215,22 +215,3 @@ func TestGetDeclarativeDir(t *testing.T) { assert.Equal(t, DeclarativeDir, GetDeclarativeDir()) }) } - -func TestSuggestClaudePlugin(t *testing.T) { - // In `go test`, os.Stdout is a pipe (not a TTY), so this exercises the - // non-interactive guard that prevents the hint from ever reaching - // captured/redirected output. - t.Run("suppresses hint when stdout is non-tty", func(t *testing.T) { - t.Setenv("CLAUDECODE", "1") - t.Setenv("CLAUDE_CODE", "") - - assert.Equal(t, "", SuggestClaudePlugin()) - }) - - t.Run("returns empty string when not running in claude code", func(t *testing.T) { - t.Setenv("CLAUDECODE", "") - t.Setenv("CLAUDE_CODE", "") - - assert.Equal(t, "", SuggestClaudePlugin()) - }) -}