Gemini CLI 終了コマンドを実測——Ctrl+Dも2回押し、入力中は3回

目次
Gemini CLI の対話画面から出られず、ターミナルのタブごと閉じたことはないだろうか。公式ドキュメントには終了方法が /quit、Ctrl+C 2回、Ctrl+D と3通り書かれているが、「入力中に押したら」「認証ダイアログが出ているときは」「2回目をゆっくり押したら」の挙動は書かれていない。抜けられない原因が押し方なのか状態なのか不具合なのかを切り分けるには、実際にキーを送って確かめるしかない。
測り方:擬似TTYにキーを打ち込むスクリプトを作った
検証環境は macOS 26.6.1 / Node v22.21.1 / @google/gemini-cli 0.63.0。終了操作は手で押しても画面を見ることしかできず、「2回目を何秒後に押したか」の再現が難しい。そこで Pythonの pty で gemini を擬似端末の子プロセスとして起動し、\r(Enter)・\x03(Ctrl+C)・\x04(Ctrl+D)を指定時刻に送り込むドライバを作った。記録するのは3点——プロセスが終了したか、終了コードが何か、画面に何が出ていたか。各試行は新規プロセスで行う。
$ ./node_modules/.bin/gemini --version
0.63.0
...
# exit=0 (2204 ms)
環境変数 HOME を検証用ディレクトリに差し替え、ダミーの GEMINI_API_KEY を与えて認証選択を通過させた(APIキー認証は選択時点では検証されず、入力済みのまま Enter で進めばチャットUIに着いた)。また GEMINI_CLI_NO_RELAUNCH=1 を指定して、gemini が内部で張る中継プロセスを外し、終了コードを直接取れるようにした。
ここで1回つまずいた。最初は起動してUIが描画されてから約2秒で、エラーメッセージ無しに終了コード1で落ちた。process.exit をフックして追うと、到達していたのは uncaughtException のクリーンアップ経路で、投げられていたのは fs.watch の EMFILE だった。
$ cat rundir/traced-exc.txt rundir/traced-watch.txt
uncaughtException: Error: EMFILE: too many open files, watch
at FSWatcher._handle.onchange (node:internal/fs/watchers:214:21)
...
fs.watch: <repo>/.git
# exit=0 (6 ms)
監視対象はリポジトリの .git だった。ブランチ名表示のためにgitディレクトリを探す処理が、このサンドボックス環境では上限を踏んだと見られる。GIT_CEILING_DIRECTORIES で上位探索を止めて回避した。一般の終了仕様ではないが、「エラーを見せずに exit 1 で落ちる」ことがある点は記録に値する。
/quit と /exit は exit 0 で終わり、セッション統計を出す
入力欄に /quit と打って Enter を押すと、プロセスは終了コード0で終わった。
$ python3 probe.py quit
=== PROBE quit ===
keys_sent=[(8, b'\r'), (13, b'\r'), (22, b'/quit\r'), (23.5, b'\r')]
RESULT: exited=True exit_code=0 elapsed=23.75s
...
# exit=0 (24454 ms)
終了時には「Agent powering down. Goodbye!」と Interaction Summary が出る。何も会話していないセッションでも Tool Calls・Success Rate・Wall Time・Session ID が出て、gemini --resume <Session ID> で同じ会話を再開できる。/exit も同じ画面を出して exit 0 で終わった(エイリアス)。コマンドで終わる利点は、キー押下の回数や猶予を気にしなくてよいこと、そして終了が正常終了だと exit 0 で確かめられることだ。
Ctrl+C は1回では終わらない。2回目の猶予は実測3秒
入力が空の状態で Ctrl+C を1回押すと、「Press Ctrl+C again to exit.」と出るだけでプロセスは生き続けた。
$ python3 probe.py single-ctrlc
=== PROBE single-ctrlc ===
keys_sent=[(8, b'\r'), (13, b'\r'), (22, b'\x03')]
RESULT: exited=False exit_code=None elapsed=38.12s
...
Press Ctrl+C again to exit.
...
# exit=0 (41907 ms)
0.3秒の間隔で2回押すと exit 0 で終了した。では2回目はどれくらい遅れても有効なのか。間隔を変えて9回測った。
$ python3 summarize.py
log gap_seconds exited exit_code
015-double-ctrlc-300ms.txt 0.3 True 0
021-gap-2s.txt 2.0 True 0
022-gap-1s.txt 1.0 True 0
023-gap-05s.txt 0.5 True 0
024-gap-3s.txt 3.0 True 0
025-gap-5s.txt 5.0 False None
026-gap-7s.txt 7.0 False None
027-gap-4s.txt 4.0 False None
028-gap-35s.txt 3.5 False None
# exit=0 (33 ms)
3.0秒までは終了し、3.5秒では終了しない。つまり2回目の猶予は実測3秒だ。インストールしたバンドル内の対応する定数を見ると WARNING_PROMPT_DURATION_MS = 3e3 で、実測と一致する。
$ grep -rn 'WARNING_PROMPT_DURATION_MS = ' node_modules/@google/gemini-cli/bundle/chunk-HFRGWL72.js
node_modules/@google/gemini-cli/bundle/chunk-HFRGWL72.js:9722:var WARNING_PROMPT_DURATION_MS = 3e3;
# exit=0 (11 ms)
「Ctrl+C 2回で終了」という記述は多いが、2回目の猶予が3秒であることは見つけられなかった。逆に言えば、4秒以上ゆっくり押しても終わらない——1回目として数え直されるだけだ。
Ctrl+D も実は2回押しだった
公式のキーボードショートカット表は Ctrl+D を「入力バッファが空のときCLIを終了」と説明している。1回押せば終わると読める。だが実測では、1回目の Ctrl+D は終了せず、「Press Ctrl+D again to exit.」という警告が出た。
$ python3 probe.py ctrld
=== PROBE ctrld ===
keys_sent=[(8, b'\r'), (13, b'\r'), (22, b'\x04')]
RESULT: exited=False exit_code=None elapsed=40.09s
...
# exit=0 (43975 ms)
0.3秒間隔の2回押しでは exit 0 で終了した。Ctrl+D も Ctrl+C と同じ「3秒以内の2回押し」ルールで、認証ダイアログ表示中に押した1回目も同じ警告が出た。ショートカット表を信じて Ctrl+D を1回だけ押して終わらない、というハマり方は表の読みどおりに操作した人ほど起きる。
入力中は回数が変わる——Ctrl+Cは3回、Ctrl+Dは効かない
hello と入力した状態で試すと、キーごとに挙動が違った。Ctrl+D は何も起きなかった(入力は残り、警告も終了もない)。
$ python3 probe.py ctrld-text
=== PROBE ctrld-text ===
keys_sent=[(8, b'\r'), (13, b'\r'), (22, b'hello'), (23.5, b'\x04')]
RESULT: exited=False exit_code=None elapsed=40.08s
...
# exit=0 (43807 ms)
Ctrl+C は1回目で入力が消えた。そこから0.5秒後に2回目を押しても終了せず、「Press Ctrl+C again to exit.」と空の入力欄が残った。同じ条件で3回押すと exit 0 で終了した。
$ python3 probe.py ctrlc-text3
=== PROBE ctrlc-text3 ===
keys_sent=[(8, b'\r'), (13, b'\r'), (22, b'hello'), (23.5, b'\x03'), (24.0, b'\x03'), (24.3, b'\x03')]
RESULT: exited=True exit_code=0 elapsed=24.60s
...
# exit=0 (25334 ms)
つまり入力中の Ctrl+C は「入力を消す」役目を先に果たし、終了にはそのあとの2回が要る。合計3回だ。
「終わらない」ときの判定表と、確かめられなかったこと
0.63.0 で確かめた成否を状態別にまとめる。
| 状態 | /quit /exit |
Ctrl+C | Ctrl+D | Esc |
|---|---|---|---|---|
| 入力が空 | exit 0 | 3秒以内に2回で exit 0 | 3秒以内に2回で exit 0 | 未検証 |
| 入力中 | exit 0 | 3回で exit 0(1回目は入力消去) | 無反応 | 未検証 |
| 認証ダイアログ中 | プロンプトが無い | 2回で exit 0 | 1回では警告のみ | 終了しない |
認証ダイアログのような全面モーダルでも Ctrl+C 2回は効いた。「Ctrl+Cで終了できない」という過去の報告(本バージョンでは再現しなかった挙動)に遭遇している人は、ダイアログ中ならまず Ctrl+C 2回を試す価値がある。
確かめられなかったことも書く。応答生成中の Ctrl+C(ダミー認証のため実際の応答を返せず、測れなかった)、v0.63.0 以外のバージョン、macOS 以外の環境、そして先ほどの EMFILE クラッシュがこのサンドボックス環境固有かどうか——これらはこの記事の範囲外だ。
読者の問いに答える。「終われない」ときの大半は押し方の問題である。まず入力を空にして(Ctrl+Cを1回)、3秒以内にもう2回。それでも駄目なら /quit と打つ——スラッシュコマンドは回数制限の対象外で、打ち込んで Enter すれば1発だ。ダイアログが全面に出ているなら、直接 Ctrl+C を2回でよい。これらはすべて終了コード0の正常終了で、Interaction Summary が出て、セッションは gemini --resume で再開できる。「終了コマンドが効かない」と思ったとき、まず疑うべきは「1回しか押していない」か「入力が残っていた」かのどちらかだ。