Skip to content

Don't prompt from git hooks when a TUI owns the terminal - #1907

Open
Soph wants to merge 1 commit into
mainfrom
soph/tui-raw-mode-noninteractive
Open

Don't prompt from git hooks when a TUI owns the terminal#1907
Soph wants to merge 1 commit into
mainfrom
soph/tui-raw-mode-noninteractive

Conversation

@Soph

@Soph Soph commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

https://entire.io/gh/entireio/cli/trails/981

Problem

A user reported that committing through lazygit froze on the interactive "Link this commit to session context?" question, hit the first time Entire ran in a new repo.

lazygit (like gitui, tig) runs git commit as a captured child while holding the controlling terminal in raw mode for its own screen and keys. The hook inherits that terminal, so /dev/tty opens fine — but it must not be used: our write paints over lazygit's UI, and our line read races lazygit's key reader for the keystroke, so the answer never arrives and the commit appears to hang.

Fix

interactive.CanPromptInteractively() now also checks the mode of the /dev/tty it already opens. Canonical input off (ICANON) means a full-screen program owns the screen, not a shell we can prompt. Suppressed prompts fall through to the existing non-interactive path, so the hook auto-links the commit rather than blocking.

Why this signal:

  • No env sentinel exists. lazygit runs git with the plain inherited environment plus GIT_OPTIONAL_LOCKS=0; its only GIT_TERMINAL_PROMPT=0 (which we already detect) is on the reset/branch paths, not commit. There is nothing lazygit-specific to key off.
  • Generic, not a name list. Terminal mode catches any TUI parent, and it tracks the distinction the TUIs themselves draw — lazygit restores cooked mode whenever it deliberately hands the terminal to a child (editor, interactive rebase, custom commands), which is exactly when prompting works and should still happen.
  • Not an isatty check on the hook's descriptors. git's hook stdio plumbing varies by version (stdin is /dev/null; stdout/stderr may be inherited or captured), while terminal ownership does not.
  • Fails open. If the mode can't be read, prompting stays enabled, so the check can only ever suppress prompts we positively know are unusable.

Verification

Probe hook on git 2.54 (macOS), measuring what a real hook sees:

hook context /dev/tty opens terminal mode prompts?
human shell running git commit -m yes icanon echo yes (unchanged)
TUI owning the terminal (lazygit shape) yes -icanon -echo no (was: hang)
agent / CI no no (unchanged)
  • End-to-end: a binary calling the real predicate returns true under a cooked pty, false under a raw pty, false with no tty.
  • New pty-based unit test covers cooked → raw → cooked on a real terminal; a second test pins the fail-open path.
  • ENTIRE_TEST_TTY=1 is still checked first, so existing tests that force the interactive commit path are unaffected.
  • 8532 unit, 448 integration, and both canary suites pass; golangci-lint clean.

Windows has no /dev/tty, so this prompt never fired there either way.

Note for reviewers

github.com/creack/pty moves from the module graph into go.mod as a direct (test-only) dependency for the pty test.

One known follow-up, deliberately out of scope: strategy/checkpoint_policy.go and hooks_git_cmd.go use this same predicate to decide print-vs-log for one-shot stderr warnings, so those warnings now downgrade to log-only under a TUI. Swapping them to a writer-scoped check wouldn't help (lazygit pipes git's stderr, so that's false too); restoring visibility means printing unconditionally, as warnIfAttributionDiverged already does. That's a per-site design call better made on its own.

🤖 Generated with Claude Code


Note

Low Risk
Narrow change to interactive detection with fail-open behavior; main effect is fewer prompts in TUI git contexts, with normal shell commits unchanged.

Overview
Fixes commits freezing when Entire tries to prompt (e.g. “Link this commit to session context?”) from a git hook while the user is in a TUI such as lazygit. Those clients keep the controlling terminal in raw mode; /dev/tty still opens, but prompting corrupts the UI and races for input.

interactive.CanPromptInteractively() still follows the existing precedence (ENTIRE_TEST_TTY, tests, agent env, CI), but step 5 no longer stops at “/dev/tty opens.” It reads the terminal termios and returns false when canonical input is off (ICANON clear), treating that as “a full-screen program owns the terminal.” Hooks then use the existing non-interactive path (e.g. auto-link) instead of blocking.

Implementation is split across ttyInRawMode on darwin/linux (TIOCGETA / TCGETS), fail-open on ioctl failure or non-Unix builds, plus pty-based tests and CLAUDE.md documentation. github.com/creack/pty is added as a direct test dependency.

Reviewed by Cursor Bugbot for commit 508c438. Configure here.

Committing through lazygit froze on the "Link this commit to session
context?" question the first time Entire ran in a repo. TUI git clients
run `git commit` as a captured child while holding the controlling
terminal in raw mode, so the hook inherits a /dev/tty that opens fine but
must not be used: our write paints over their screen and our line read
races their key reader for the keystroke, so the answer never arrives.

CanPromptInteractively now also checks the mode of the /dev/tty it
already opens — canonical input off means a full-screen program owns the
screen, not a shell we can prompt. Suppressed prompts take the existing
non-interactive path, so the hook auto-links the commit instead of
blocking. lazygit sets no usable env var on its commit path, so there is
no sentinel to key off; the mode check is also generic (gitui, tig, any
TUI) and tracks what the TUIs themselves do, since they restore cooked
mode whenever they deliberately hand the terminal to a child.

Terminal mode rather than an isatty check on the hook's descriptors:
git's hook stdio plumbing varies by version (stdin is /dev/null, and
stdout/stderr may be inherited or captured), while terminal ownership
does not. Detection fails open whenever the mode can't be read, so it can
only ever suppress prompts we positively know are unusable.

Measured with a probe hook on git 2.54: a human shell leaves the terminal
canonical, a raw-mode parent does not; verified end to end that the
predicate returns true under a cooked pty and false under a raw one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KZ9JKKYRJZE534P7J9RN43RW
@Soph
Soph requested a review from a team as a code owner August 5, 2026 18:23
Copilot AI lite review requested due to automatic review settings August 5, 2026 18:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a deadlock/hang scenario when entire is invoked from git hooks under terminal-UIs (e.g., lazygit) that keep the controlling TTY in raw mode. It extends interactive.CanPromptInteractively() to treat a raw-mode controlling terminal as non-interactive, forcing hooks down the existing non-interactive/autofallback paths.

Changes:

  • Extend /dev/tty detection to also check terminal mode (ICANON) and suppress prompts when the controlling terminal is held in raw mode by a parent TUI.
  • Add Unix-specific ioctl implementations (darwin/linux) with a fail-open fallback on unsupported platforms.
  • Add pty-backed unit test coverage (and a non-terminal “fail open” test), introducing github.com/creack/pty as a test dependency.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
go.mod Adds github.com/creack/pty (used by new pty-based tests).
cmd/entire/cli/interactive/interactive.go Updates CanPromptInteractively() to return false when /dev/tty is in raw mode.
cmd/entire/cli/interactive/rawmode_unix.go Implements raw-mode detection via termios ioctl; fail-open on ioctl errors.
cmd/entire/cli/interactive/rawmode_linux.go Defines Linux ioctl constant (TCGETS).
cmd/entire/cli/interactive/rawmode_darwin.go Defines Darwin ioctl constant (TIOCGETA).
cmd/entire/cli/interactive/rawmode_other.go Provides fail-open stub for non-darwin/non-linux platforms.
cmd/entire/cli/interactive/rawmode_test.go Verifies fail-open behavior on non-terminal files.
cmd/entire/cli/interactive/rawmode_pty_test.go Verifies canonical→raw→canonical transitions on a real pty.
CLAUDE.md Updates documentation to reflect the new terminal-mode check in prompting precedence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants