[BUG] Ctrl+G editor is spawned from the bg-spare daemon without a controlling terminal - emacs cannot launch (exit 1)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
With EDITOR=emacs (or emacs -nw), pressing Ctrl+G fails immediately with Emacs quit unexpectedly (exit code 1) — the editor never appears. vim works in the same setup, which makes this look like an emacs problem; it isn't.
Root cause: since roughly 2.1.139, interactive sessions can be adopted by a pre-warmed daemon process (claude bg-spare, a child of claude bg-pty-host — visible in ps with ?? in the TTY column) instead of running in the foreground claude process. On my machine this happens consistently; when a session does run in-process, Ctrl+G works fine, which may explain why reports of this are inconsistent across setups.
The editor is spawned from that daemon process with spawnSync(..., {stdio: "inherit"}). The child inherits fds pointing at the session pty but has no controlling terminal, because nothing in the daemon's process tree ever acquired one. Most editors don't care; emacs does — during terminal init it opens the literal device /dev/tty, which requires a controlling terminal, so it exits on the spot.
The timing lines up with #58664, which bisected a Ctrl+G regression to the 2.1.132 → 2.1.138/139 window, exactly when the daemon architecture shipped. #19076 (nano/micro failing over ssh) may be related; both were closed by the stale-bot without a diagnosis. #83006 shows the same error string but is a different bug (content discarded when an editor exits nonzero) — filing separately since this one is about the editor being unable to launch at all.
What Should Happen?
Ctrl+G opens the editor named by $EDITOR/$VISUAL, as it does when the session runs in-process (and as it did before ~2.1.138).
Error Messages/Logs
Emacs quit unexpectedly (exit code 1)
# and, captured from the spawned emacs's stderr:
emacs: Could not open file: /dev/tty
Steps to Reproduce
- macOS,
EDITOR=emacsexported (emacs 30.x, terminal build). - Start
claudein a terminal and confirm the session is daemon-hosted:ps ax -o pid,ppid,tty,command | grep bg-spareshows aclaude bg-spareprocess with??in the TTY column hosting the session. - Press Ctrl+G.
- The error appears immediately; emacs never launches. Pointing
EDITORat a logging wrapper shows the spawned process has stdin on the session pty butopen("/dev/tty")fails, because the process has no controlling terminal. - Counterexample: in a session that runs in-process (no
bg-spareancestor), the same Ctrl+G works fine.
Observed over ssh (Ghostty) and reproduced locally; the launch failure does not depend on the terminal emulator.
Claude Model
Any
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.132 (last confirmed working per #58664's bisect)
Claude Code Version
2.1.239 (Claude Code)
Platform
Claude Code CLI
Operating System
macOS
Terminal/Shell
Ghostty
Additional Information
Workaround, part 1 — get emacs to start. Emacs's -t DEVICE flag opens a named terminal device instead of /dev/tty, and stdin is a valid pty in the daemon spawn. Wrapper as $EDITOR:
#!/bin/sh
if t=$(tty); then
exec emacs -nw -t "$t" "$@"
fi
exec emacs -nw "$@" </dev/tty >/dev/tty 2>/dev/tty
Workaround, part 2 — fix the keyboard (ssh + Ghostty). With the wrapper above, emacs opens, but in my setup every modified key then arrived CSI-u encoded (Ctrl-C showed up as literal ;5u text, so there was no way to quit). I confirmed the kitty-protocol flags were 0 immediately before emacs started, yet modified keys arrive CSI-u encoded once emacs is up — possibly emacs's own modifyOtherKeys handshake combined with how Ghostty encodes that mode; I haven't pinned down which layer enables it. Teaching emacs to decode it fixes the symptom regardless of the cause; shim loaded via -l:
;; csi-u-decode.el: map CSI-u key sequences to normal emacs key events
(defun csi-u--event (code mods)
(let ((bits (1- mods)))
(event-convert-list
(append (when (/= 0 (logand bits 4)) '(control))
(when (/= 0 (logand bits 2)) '(meta))
(when (/= 0 (logand bits 1)) '(shift))
(list code)))))
(dolist (code (append (number-sequence 32 126) '(9 13 27 127)))
(dolist (mods (number-sequence 2 16))
(ignore-errors
(define-key input-decode-map (format "\e[%d;%du" code mods)
(vector (csi-u--event code mods))))))
(define-key input-decode-map "\e[27u" [?\e])
So the happy path becomes exec emacs -nw -t "$t" -l /path/to/csi-u-decode.el "$@". With both pieces, Ctrl+G → edit → C-x C-c round-trips correctly through the daemon.
Suggested fix: give the spawned editor a controlling terminal before exec — the standard login_tty(3) sequence (setsid(), then ioctl(TIOCSCTTY) on the pty), the same thing tmux does for processes it spawns on ptys it owns. In a synthetic reproduction of the daemon's spawn topology both calls succeed (the spawned child is not a session leader and the pty is unclaimed), so this looks like a small change. There's currently no user-facing way to disable the daemon/spare architecture, so until then a wrapper is the only client-side option.