Skip to content
Latchkey

"tsx watch" Exits Immediately in CI - Fix Watch-Mode in Pipelines

Watch-mode runners like tsx watch are built to stay running and re-run on file changes. In CI that is the wrong shape - the step either hangs forever waiting for changes or exits immediately when stdin closes, neither of which is a real run.

What this error means

A CI step invoking tsx watch src/index.ts either never returns (the job times out) or returns instantly with the process gone, because the watcher had no TTY and exited. The same command works on a developer machine.

CI log
$ tsx watch src/index.ts
# CI: hangs until the job timeout kills it
# or exits at once because stdin/TTY closed

Common causes

Watch mode used where a one-shot run is needed

watch keeps the process alive to react to edits. CI wants the script to run once and exit with a status - the two are fundamentally different lifecycles.

No TTY/stdin on the runner

Some watchers tie their lifetime to stdin or a TTY. On a headless runner those are absent, so the watcher exits at once or behaves erratically.

How to fix it

Run once, not in watch mode

Use the non-watch invocation in CI and reserve watch for the local dev script.

package.json
// package.json
{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "tsx src/index.ts"
  }
}
// CI: npm run start  (not the dev/watch script)

Separate dev and CI scripts

  1. Keep watch only in the dev script developers run interactively.
  2. Have CI call the plain runner (tsx, node, or the built output).
  3. Never put a watcher on the critical path of a pipeline step.

How to prevent it

  • Reserve --watch/watch for local development scripts.
  • Run a single-shot command in CI and let it exit with a status.
  • Add a job-level timeout so an accidental watcher cannot hang forever.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →