Summary
Every invocation of the openapi binary, including openapi --version, writes an OSC 11 background-colour query and a cursor-position request (ESC ] 11 ; ? ESC \ then ESC [ 6 n) to the terminal before doing anything else, then waits up to 5 s for a reply. In an interactive terminal the reply arrives immediately and is consumed. Under anything that hands the CLI a pseudo-terminal without relaying the reply, which is how pre-commit runners such as lefthook run hooks, the CLI blocks for the full timeout and the escape sequences land in the hook's captured output. When the real terminal does answer, the CLI never consumes the reply and it shows up as junk like 11;rgb:2424/2424/2424;1R in the shell.
This is not the CLI's own code. cmd/openapi/go.mod requires github.com/charmbracelet/bubbletea v1.3.10, and bubbletea v1's tea_init.go calls lipgloss.HasDarkBackground() from a package init(), so the query fires for any binary that links bubbletea, TUI or not. termenv's OSCTimeout is a hard-coded 5 s. Upstream tracked this as charmbracelet/bubbletea#1771 and closed it as not planned because v2 no longer queries at init.
Reproduction
macOS, TERM=xterm-256color, openapi built with go install github.com/speakeasy-api/openapi/cmd/openapi@latest (v0.0.0-20260826005500-83ebf39fa45e). script -q /dev/null ... gives the process a pty that nothing answers.
$ printf 'openapi: 3.1.0\ninfo:\n title: t\n version: "1"\npaths: {}\n' > spec.yaml
$ /usr/bin/time -p script -q /dev/null openapi spec lint spec.yaml < /dev/null | cat -v | head -1
^[]11;?^[\^[[6nLinting OpenAPI document: spec.yaml^M
real 5.02
$ CI=1 /usr/bin/time -p script -q /dev/null openapi spec lint spec.yaml < /dev/null | cat -v | head -1
Linting OpenAPI document: spec.yaml^M
real 0.02
$ /usr/bin/time -p script -q /dev/null openapi --version < /dev/null | cat -v
^[]11;?^[\^[[6nv0.0.0-20260826005500-83ebf39fa45e
real 5.02
CI=1 helps because termenv's isTTY() returns false whenever CI is set, which skips the query. From a lefthook pre-commit hook on the same machine I measured about 5.8 s with the reply leaking into the output, and 0.4 s with CI=1.
Expected
openapi spec lint and openapi --version don't touch the terminal or pay a timeout unless a TUI is actually about to start.
Suggested fix
The query runs at package init, before main(), so the CLI can't gate it on "is this really an interactive TTY" from its own code, and setting CI from inside the binary is too late as well. The options I see:
- Move to bubbletea v2 (with the matching lipgloss/huh releases). v2 doesn't query at init, which is why upstream closed #1771.
- Until then, document
CI=1 as the escape hatch for pre-commit hooks and other non-interactive runs, since termenv already honours it.
Happy to send a PR for either once you say which way you'd like to go.
Summary
Every invocation of the
openapibinary, includingopenapi --version, writes an OSC 11 background-colour query and a cursor-position request (ESC ] 11 ; ? ESC \thenESC [ 6 n) to the terminal before doing anything else, then waits up to 5 s for a reply. In an interactive terminal the reply arrives immediately and is consumed. Under anything that hands the CLI a pseudo-terminal without relaying the reply, which is how pre-commit runners such as lefthook run hooks, the CLI blocks for the full timeout and the escape sequences land in the hook's captured output. When the real terminal does answer, the CLI never consumes the reply and it shows up as junk like11;rgb:2424/2424/2424;1Rin the shell.This is not the CLI's own code.
cmd/openapi/go.modrequiresgithub.com/charmbracelet/bubbletea v1.3.10, and bubbletea v1'stea_init.gocallslipgloss.HasDarkBackground()from a packageinit(), so the query fires for any binary that links bubbletea, TUI or not. termenv'sOSCTimeoutis a hard-coded 5 s. Upstream tracked this as charmbracelet/bubbletea#1771 and closed it as not planned because v2 no longer queries at init.Reproduction
macOS,
TERM=xterm-256color,openapibuilt withgo install github.com/speakeasy-api/openapi/cmd/openapi@latest(v0.0.0-20260826005500-83ebf39fa45e).script -q /dev/null ...gives the process a pty that nothing answers.CI=1helps because termenv'sisTTY()returns false wheneverCIis set, which skips the query. From a lefthook pre-commit hook on the same machine I measured about 5.8 s with the reply leaking into the output, and 0.4 s withCI=1.Expected
openapi spec lintandopenapi --versiondon't touch the terminal or pay a timeout unless a TUI is actually about to start.Suggested fix
The query runs at package init, before
main(), so the CLI can't gate it on "is this really an interactive TTY" from its own code, and settingCIfrom inside the binary is too late as well. The options I see:CI=1as the escape hatch for pre-commit hooks and other non-interactive runs, since termenv already honours it.Happy to send a PR for either once you say which way you'd like to go.