gemini-cli - ✅(Solved) Fix Non-interactive stdin raw mode cleanup is missing a process exit fallback [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
google-gemini/gemini-cli#27290Fetched 2026-05-20 03:59:21
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
labeled ×2cross-referenced ×1

Root Cause

This is easiest to verify with a regression test rather than a manual terminal repro, because leaving a real terminal in raw mode is annoying.

Fix Action

Fixed

PR fix notes

PR #27292: fix(cli): restore non-interactive stdin raw mode on exit

Description (problem / solution / changelog)

Summary

This PR makes the non-interactive Ctrl+C cancellation path safer by restoring stdin raw mode during process exit, not only during the normal finally cleanup path.

Today, non-interactive mode enables raw mode when process.stdin.isTTY is true so it can catch Ctrl+C. If the process exits after raw mode is enabled but before the normal cleanup path runs, the user’s terminal can be left in a bad state until they run reset or stty sane.

Details

The fix updates both non-interactive implementations:

  • packages/cli/src/nonInteractiveCli.ts
  • packages/cli/src/nonInteractiveCliAgentSession.ts

Changes made:

  • Register cleanupStdinCancellation with process.on('exit', ...).
  • Make stdin cancellation cleanup idempotent.
  • Track whether raw mode was actually enabled before trying to restore it.
  • Remove only the keypress listener registered by this path instead of clearing all stdin keypress listeners.

One small caveat: Node cannot handle literal SIGKILL, so no process-level cleanup can protect against kill -9. This still improves the normal abrupt-exit path where Node emits exit.

Related Issues

Fixes #27290

How to Validate

From the repo root:

npm run build --workspace=@google/gemini-cli-core

Then run the targeted regression tests:

cd packages/cli
../../node_modules/.bin/vitest run src/nonInteractiveCli.test.ts src/nonInteractiveCliAgentSession.test.ts

Expected result:

✓ src/nonInteractiveCli.test.ts
✓ src/nonInteractiveCliAgentSession.test.ts

Test Files  2 passed
Tests       115 passed

Also validated from the repo root:

npm run typecheck --workspace=@google/gemini-cli
npm run lint --workspace=@google/gemini-cli -- --max-warnings=0
Expected result: both commands pass.

Regression coverage added:

  • Verifies that non-interactive mode registers a process.on('exit', ...) cleanup handler.
  • Verifies that invoking the exit handler restores the previous stdin raw-mode state.
  • Verifies that the exit handler unregisters itself afterward.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any): none
  • Validated on required platforms/methods:
  • MacOS
  • npm run
  • npx
  • Docker
  • Podman
  • Seatbelt
  • Windows
  • Linux

Changed files

  • packages/cli/src/nonInteractiveCli.test.ts (modified, +90/-0)
  • packages/cli/src/nonInteractiveCli.ts (modified, +16/-3)
  • packages/cli/src/nonInteractiveCliAgentSession.test.ts (modified, +90/-0)
  • packages/cli/src/nonInteractiveCliAgentSession.ts (modified, +16/-3)
RAW_BUFFERClick to expand / collapse

What happened?

While reviewing the non-interactive Ctrl+C cancellation path, I noticed that setupStdinCancellation() enables raw mode whenever process.stdin.isTTY is true, but the raw mode restoration only happens through the normal finally cleanup path.

The affected paths are:

  • packages/cli/src/nonInteractiveCli.ts
  • packages/cli/src/nonInteractiveCliAgentSession.ts

That means if the process exits abruptly after process.stdin.setRawMode(true) succeeds but before cleanupStdinCancellation() runs normally, the user’s terminal can be left in raw mode. In that state, the terminal feels broken until the user runs something like reset or stty sane.

Small nuance: SIGKILL itself cannot be handled by Node, so this cannot protect against literal kill -9. But registering the stdin cleanup on process.on('exit', ...) gives a best-effort fallback for normal Node process exits after raw mode has been enabled.

What did you expect to happen?

Once non-interactive mode enables stdin raw mode, Gemini CLI should register a synchronous, idempotent cleanup path immediately so raw mode is restored on process exit as well as on the normal finally path.

Expected behavior:

  • Save the original raw-mode state before enabling raw mode.
  • Restore that state during normal cleanup.
  • Also restore that state from a process.on('exit', cleanupStdinCancellation) handler.
  • Remove only the keypress listener registered by this code path, rather than clearing all keypress listeners on stdin.

Steps to reproduce

This is easiest to verify with a regression test rather than a manual terminal repro, because leaving a real terminal in raw mode is annoying.

Code path:

  1. Run Gemini CLI in non-interactive mode with TTY stdin.
  2. setupStdinCancellation() calls process.stdin.setRawMode(true).
  3. Simulate process exit before the normal finally cleanup path completes.
  4. Observe that the previous implementation had no process.on('exit', ...) handler to restore raw mode.

A regression test can assert that:

  • an exit handler is registered,
  • raw mode is enabled,
  • invoking the exit handler restores the previous raw-mode state,
  • the handler unregisters itself afterward.

Client information

Not specific to a model, auth provider, or Gemini API response.

Platform observed during development:

  • macOS
  • Node.js 20+
  • Gemini CLI local checkout

This bug is in local terminal/stdin lifecycle handling, so it can affect any login mode.

Login information

Not login-specific.

This happens before the auth/model path matters and applies equally to Google account, API key, or Vertex AI usage.

Anything else we need to know?

I have a small fix and regression coverage ready.

The fix registers cleanupStdinCancellation with process.on('exit', ...) in both non-interactive implementations and makes the cleanup idempotent. It also removes only the owned keypress listener instead of calling removeAllListeners('keypress').

Targeted verification passed locally:

<img width="936" height="582" alt="Image" src="https://github.com/user-attachments/assets/9d1400c3-b920-4549-a556-6a5321f060dc" />

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

gemini-cli - ✅(Solved) Fix Non-interactive stdin raw mode cleanup is missing a process exit fallback [1 pull requests, 1 participants]