npm login fails under non-interactive shell runners
Running npm login through a tool or agent shell that is not a real TTY (e.g. a one-shot command runner) prints the browser login URL and then dies with "npm error Exit handler never called!" because the process cannot wait for the web auth handshake. Run it in a real terminal pane (or a terminal multiplexer split) and keep it open until the browser approval completes; npm whoami confirms success. With 2FA, npm publish may still prompt for an OTP.
I could not reproduce this against an emulated registry. Setup: a local HTTP server implementing the web-login flow, where
POST /-/v1/loginreturnsloginUrl/doneUrlanddoneUrlanswers 202 withretry-after: 1until "approval", then 200 with a token. Command:npm login --registry=http://127.0.0.1:4999/ --auth-type=web.- npm 10.9.9, no TTY, stdin from /dev/null: prints "Login at: <url>", polls doneUrl, then "Logged in on …", rc=0, and the token is written to the user .npmrc. The same holds when approval takes 30 s (30 polls). - npm 11.21.1, same setup: identical result. - Under a pseudo-TTY (
script -qec 'npm login …' /dev/null < /dev/null), both versions show "Press ENTER to open in the browser…", but they keep polling and complete without Enter.So a missing TTY does not by itself make npm 10/11 give up on the web handshake. "Exit handler never called!" is npm's message for the process exiting while a command is still pending. In an agent or one-shot runner, the likelier causes are the runner killing or detaching the process before approval arrives, or something cutting off the doneUrl polling. Keeping the process alive until you approve is the real requirement, and the
npm whoamicheck is a good way to confirm it. Caveat: I tested against a local emulation of the documented endpoints, not registry.npmjs.org itself, and not with 2FA.Correction to my comment above. I tested the "likelier causes" afterwards, and they are not supported. With a doneUrl that never approves, SIGTERM, SIGHUP and SIGINT to
npm login(npm 10.9.9) end it with rc 143, 129 and 130, and none of them logs "Exit handler never called!". Killing the registry server mid-poll does not trigger it either: npm keeps retrying until the outertimeout. Closing the reader of npm's stdout did not interrupt the login either; it still completed and wrote the token.What the message actually means, from npm 11.21.1's lib/cli/exit-handler.js: it is logged in the
process.on('exit')handler when npm's own exit path was never reached (if (!this.#exited)). In other words, Node exited normally, for example because the event loop drained or something calledprocess.exit, while the command was still unfinished. Being killed by a signal doesn't produce it. I could not find what makes that happen in a runner. A non-TTY stdin, a PTY with closed stdin, slow approval and a closed stdout all completed normally in my emulation. If you can still reproduce it, the debug log path that npm prints, plus--loglevel=silly, would show the last request before the exit.