Symptom
hookdeck listen connects, forwards events, and never says it connected.
The documented readiness line — Connected. Waiting for events..., promised in README § Output modes for both compact and quiet — is not printed when stdout/stderr are pipes, or when --color off is passed. Nothing else is printed either, so a caller waiting on the documented signal has no way to tell a healthy tunnel from a broken one and eventually reports a connection timeout.
Reported from the field: a wrapper ran
hookdeck listen 28765 <source> <connection> --path /… --output compact --color off --log-level info
waited 45s for the readiness line, saw nothing, and filed it as a websocket failure. The websocket was fine the whole time.
Cause
SimpleRenderer.OnConnected puts its entire body — the readiness line and the active-filters notice — behind if r.spinner != nil (pkg/listen/proxy/renderer_simple.go:47).
r.spinner is set from ansi.StartNewSpinner, which returns nil when !isTerminal(w) || !shouldUseColors(w) (pkg/ansi/ansi.go:134). So the spinner is nil, and the readiness line is dropped, whenever:
--color off is passed (ansi.DisableColors, pkg/config/config.go:174) — even on a full TTY; or
- the writer is not a terminal.
The writer is log.StandardLogger().Out, i.e. stderr. Nothing in the CLI calls SetOutput, so whether a line reaches stdout is decided by whether stderr is a TTY. A harness that gives stdout a PTY but pipes stderr loses the line too.
OnDisconnected has the same shape: Connection lost, reconnecting... goes to stderr via the spinner fallback while stdout receives only a bare newline.
Related: #333 downgrades --output interactive to compact when stdout is not a terminal, so non-interactive callers are steered onto exactly the renderer whose readiness line a non-interactive environment suppresses.
Affected
All 2.x through v2.5.0, and release/v3.0.0 — pkg/ansi/ansi.go and pkg/listen/proxy/renderer_simple.go are byte-identical between v2.5.0 and that branch.
Reproduction
Built from the v2.5.0 tag, run against a local stand-in for api.hookdeck.com / ws.hookdeck.com, with a real PTY via script.
With --color off, on a PTY — server side is healthy throughout:
[mock] API POST /2025-07-01/cli-sessions body: {"webhook_ids":["web_test"]}
[mock] WS upgrade websocket-id=cses_test x-webhook-ids=web_test
[mock] WS handshake completed (101)
[mock] WS ping received -> pong (x14, for minutes)
Terminal side, for the whole run:
Listening on
repro-source
│ Requests to → https://hkdk.events/src_test
└─ Forwards to → http://localhost:28765/v1/tradingview-alert (cli-repro-source)
Getting ready...
Same command without --color off: ⣾ Getting ready... Connected. Waiting for events...
No --color flag at all, stdout and stderr piped: readiness line absent from stdout; Getting ready... on stderr. The flag is not required to hit this — any CI job, tee, or agent harness reproduces it.
Telling the three outcomes apart (for triage)
Useful when someone reports "listen never connects":
| What actually happened |
What you see |
| Connected; readiness line suppressed (this bug) |
banner, then nothing, process still running past ~30s |
| Websocket unreachable |
spinner ~20s, then Could not connect. Terminating after 10 failed attempts to establish a connection., exit 1 |
| Server rejecting the session (close 4001) |
repeated Connection lost, reconnecting... on stderr, bare blank lines on stdout |
Only the first is silent-but-alive. If a report says the process was still running quietly after 30–45 seconds, the tunnel was up.
Fix
Print connection state unconditionally, and put it on stdout next to the banner and the event log, so one stream carries the whole state machine. The spinner stays exactly as it is when there is a terminal to draw on.
Also worth doing, but out of scope for a patch: the reason a dial failed is only ever logged at debug, so the Could not connect message above never says why (no "connection refused", no status code).
Symptom
hookdeck listenconnects, forwards events, and never says it connected.The documented readiness line —
Connected. Waiting for events..., promised in README § Output modes for bothcompactandquiet— is not printed when stdout/stderr are pipes, or when--color offis passed. Nothing else is printed either, so a caller waiting on the documented signal has no way to tell a healthy tunnel from a broken one and eventually reports a connection timeout.Reported from the field: a wrapper ran
waited 45s for the readiness line, saw nothing, and filed it as a websocket failure. The websocket was fine the whole time.
Cause
SimpleRenderer.OnConnectedputs its entire body — the readiness line and the active-filters notice — behindif r.spinner != nil(pkg/listen/proxy/renderer_simple.go:47).r.spinneris set fromansi.StartNewSpinner, which returnsnilwhen!isTerminal(w) || !shouldUseColors(w)(pkg/ansi/ansi.go:134). So the spinner is nil, and the readiness line is dropped, whenever:--color offis passed (ansi.DisableColors,pkg/config/config.go:174) — even on a full TTY; orThe writer is
log.StandardLogger().Out, i.e. stderr. Nothing in the CLI callsSetOutput, so whether a line reaches stdout is decided by whether stderr is a TTY. A harness that gives stdout a PTY but pipes stderr loses the line too.OnDisconnectedhas the same shape:Connection lost, reconnecting...goes to stderr via the spinner fallback while stdout receives only a bare newline.Related: #333 downgrades
--output interactivetocompactwhen stdout is not a terminal, so non-interactive callers are steered onto exactly the renderer whose readiness line a non-interactive environment suppresses.Affected
All 2.x through v2.5.0, and
release/v3.0.0—pkg/ansi/ansi.goandpkg/listen/proxy/renderer_simple.goare byte-identical betweenv2.5.0and that branch.Reproduction
Built from the
v2.5.0tag, run against a local stand-in forapi.hookdeck.com/ws.hookdeck.com, with a real PTY viascript.With
--color off, on a PTY — server side is healthy throughout:Terminal side, for the whole run:
Same command without
--color off:⣾ Getting ready... Connected. Waiting for events...No
--colorflag at all, stdout and stderr piped: readiness line absent from stdout;Getting ready...on stderr. The flag is not required to hit this — any CI job,tee, or agent harness reproduces it.Telling the three outcomes apart (for triage)
Useful when someone reports "listen never connects":
Could not connect. Terminating after 10 failed attempts to establish a connection., exit 1Connection lost, reconnecting...on stderr, bare blank lines on stdoutOnly the first is silent-but-alive. If a report says the process was still running quietly after 30–45 seconds, the tunnel was up.
Fix
Print connection state unconditionally, and put it on stdout next to the banner and the event log, so one stream carries the whole state machine. The spinner stays exactly as it is when there is a terminal to draw on.
Also worth doing, but out of scope for a patch: the reason a dial failed is only ever logged at
debug, so theCould not connectmessage above never says why (no "connection refused", no status code).