Plugin help output truncated due to SyncStdout race in CleanupAllClients · Issue #1563 · stripe/stripe-cli · GitHub
Skip to content

Plugin help output truncated due to SyncStdout race in CleanupAllClients #1563

Open

Description

@jar-stripe

Summary

When running stripe apps --help, the CLI sometimes returns only the first line of help output ("Commands for building Stripe apps\n") instead of the full Cobra help text. The exit code is 0 and stderr is empty.

Root cause

In pkg/cmd/plugin_cmds.go, the help handler calls runPluginCmd, which does:

err = plugin.Run(ctx, ptc.cfg, fs, ptc.ParsedArgs, "")
plugins.CleanupAllClients()

CleanupAllClients() calls hcplugin.CleanupClients(), which kills the plugin process. The plugin's stdout is piped to the host via go-plugin's SyncStdout goroutine (ClientConfig.SyncStdout = os.Stdout). After the gRPC RunCommand returns, the plugin has written all its help text to its stdout pipe buffer — but the SyncStdout goroutine in the host process hasn't finished copying that data to os.Stdout. When the CLI's main goroutine exits, Go terminates the SyncStdout goroutine mid-drain.

The truncation is reproducible when the plugin binary is cold (first invocation, not in OS page cache). A second invocation on the same machine reliably returns the full output.

Impact

Any stripe <plugin> --help command that goes through the go-plugin code path can be affected. Plugins with more subcommands (longer help text) are more likely to exhibit truncation because more data needs to drain through the pipe.

Steps to reproduce

  1. Clear the OS page cache for the apps plugin binary (or use a fresh CI runner)
  2. Run stripe apps --help
  3. Observe only the first line of output

Suggested fix

Wait for the SyncStdout pipe to be fully drained before exiting. Options:

  1. In plugin.Run(): after RunCommand returns, wait for the plugin process to fully exit (which closes the pipe and lets SyncStdout drain) before returning.
  2. In runPluginCmd(): add a small delay or explicit wait between plugin.Run() and CleanupAllClients() to give the SyncStdout goroutine time to finish.
  3. In the go-plugin ClientConfig: set Managed: false and manually manage the client lifecycle, calling client.Kill() only after confirming the stdout pipe has been drained.

Workaround

Retry the command if the output appears truncated (exit code 0 but missing expected content). The second invocation succeeds because the binary is now in the OS page cache.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions