From d79374a8541fdf75c89292cc78da37b8481b9964 Mon Sep 17 00:00:00 2001 From: Ossie Irondi Date: Mon, 14 Sep 2026 13:25:18 -0500 Subject: [PATCH 1/5] fix(hush): clear no-TTY error for the interactive TUI instead of crossterm's Device-not-configured exit --- CHANGELOG.md | 4 ++++ bws-tui/src/tui/terminal.rs | 17 +++++++++++++++-- bws-tui/src/tui/tests.rs | 9 +++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8965a0..e47eacb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## [Unreleased] +### Fixed + +- hush: TUI now reports a clear "no TTY" error with subcommand guidance instead of crossterm's cryptic "Device not configured" failure (#57) + ### Added - agent-native hush: exec env injection, scoped get, list --json, audit log (#54) [#54] diff --git a/bws-tui/src/tui/terminal.rs b/bws-tui/src/tui/terminal.rs index 626e042..fca9ccd 100644 --- a/bws-tui/src/tui/terminal.rs +++ b/bws-tui/src/tui/terminal.rs @@ -1,11 +1,11 @@ use super::{events, App}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{backend::CrosstermBackend, Terminal}; -use std::io::{self, Write}; +use std::io::{self, IsTerminal, Write}; #[derive(Default)] struct TerminalGuard { @@ -50,6 +50,13 @@ impl Drop for TerminalGuard { } pub(super) fn run(app: &mut App) -> Result<()> { + if !is_interactive(io::stdin().is_terminal(), io::stdout().is_terminal()) { + bail!( + "hush's interactive TUI needs a terminal session (no TTY found). \ + Run `hush` from an interactive terminal, or use the script-friendly \ + subcommands: `hush list`, `hush get --key `, `hush exec --key -- `" + ); + } let mut guard = TerminalGuard::default(); guard.enable_raw()?; let mut output = io::stdout(); @@ -60,6 +67,12 @@ pub(super) fn run(app: &mut App) -> Result<()> { finish_terminal(event, raw, screen) } +/// The TUI needs either side attached to a terminal; crossterm opens /dev/tty +/// directly for input when stdin is piped, so stdout alone is enough. +pub(super) fn is_interactive(stdin_tty: bool, stdout_tty: bool) -> bool { + stdin_tty || stdout_tty +} + pub(super) fn finish_terminal( event: Result<()>, raw: Result<()>, diff --git a/bws-tui/src/tui/tests.rs b/bws-tui/src/tui/tests.rs index 65c8cb1..f0be50b 100644 --- a/bws-tui/src/tui/tests.rs +++ b/bws-tui/src/tui/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::tui::terminal::is_interactive; use anyhow::anyhow; #[test] @@ -65,3 +66,11 @@ fn terminal_cleanup_reports_every_failure() { assert!(error.contains("raw failed")); assert!(error.contains("screen failed")); } + +#[test] +fn tui_gate_accepts_a_tty_on_either_side() { + assert!(is_interactive(true, false)); + assert!(is_interactive(false, true)); + assert!(is_interactive(true, true)); + assert!(!is_interactive(false, false)); +} From 54444335101ebc225d47296f47385fca65194a0f Mon Sep 17 00:00:00 2001 From: Ossie Irondi Date: Wed, 30 Sep 2026 21:12:34 -0500 Subject: [PATCH 2/5] docs(changelog): consolidate unreleased fixes --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47eacb..4a2a62e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,6 @@ ## [Unreleased] -### Fixed - -- hush: TUI now reports a clear "no TTY" error with subcommand guidance instead of crossterm's cryptic "Device not configured" failure (#57) - ### Added - agent-native hush: exec env injection, scoped get, list --json, audit log (#54) [#54] @@ -51,6 +47,7 @@ ### Fixed +- hush: TUI now reports a clear "no TTY" error with subcommand guidance instead of crossterm's cryptic "Device not configured" failure (#57) - remediate merged-#35 CodeRabbit findings (#36) [#35] - post-merge remediation for #33 (validator hardening, packaging hygiene, ask-codex metadata) (#34) [#33] - complete issue #10 port hardening (#33) [#10] From aa69b0de4fbfb0b6d7540be04dc286e54de4d459 Mon Sep 17 00:00:00 2001 From: Ossie Irondi Date: Wed, 30 Sep 2026 21:18:16 -0500 Subject: [PATCH 3/5] test(hush): avoid test-tail overlap with clipboard PR --- bws-tui/src/tui/tests.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bws-tui/src/tui/tests.rs b/bws-tui/src/tui/tests.rs index f0be50b..af7e0a1 100644 --- a/bws-tui/src/tui/tests.rs +++ b/bws-tui/src/tui/tests.rs @@ -53,6 +53,14 @@ fn edit_fields_cycle_without_invalid_numeric_states() { assert_eq!(EditField::Key.previous(), EditField::Note); } +#[test] +fn tui_gate_accepts_a_tty_on_either_side() { + assert!(is_interactive(true, false)); + assert!(is_interactive(false, true)); + assert!(is_interactive(true, true)); + assert!(!is_interactive(false, false)); +} + #[test] fn terminal_cleanup_reports_every_failure() { let error = terminal::finish_terminal( @@ -66,11 +74,3 @@ fn terminal_cleanup_reports_every_failure() { assert!(error.contains("raw failed")); assert!(error.contains("screen failed")); } - -#[test] -fn tui_gate_accepts_a_tty_on_either_side() { - assert!(is_interactive(true, false)); - assert!(is_interactive(false, true)); - assert!(is_interactive(true, true)); - assert!(!is_interactive(false, false)); -} From d9e9f43ba06c01a5bdbdc2ae40a1e8d5393dfda1 Mon Sep 17 00:00:00 2001 From: Ossie Irondi Date: Wed, 30 Sep 2026 21:34:26 -0500 Subject: [PATCH 4/5] fix(ci): isolate gitleaks installer temporary files --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c36ee2..1a8f1e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,8 @@ jobs: - name: gitleaks uses: gitleaks/gitleaks-action@v2 env: + # The action downloads a fixed filename; keep it in job-local temp. + TMPDIR: ${{ runner.temp }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITLEAKS_CONFIG: .gitleaks.toml From 15497da2ee4991fdd6962a55ebcfc9e0bbadf7fd Mon Sep 17 00:00:00 2001 From: Ossie Irondi Date: Wed, 30 Sep 2026 21:42:02 -0500 Subject: [PATCH 5/5] docs(hush): explain terminal gate and restoration contract --- bws-tui/src/tui/terminal.rs | 4 ++++ bws-tui/src/tui/tests.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/bws-tui/src/tui/terminal.rs b/bws-tui/src/tui/terminal.rs index fca9ccd..6aaa7e4 100644 --- a/bws-tui/src/tui/terminal.rs +++ b/bws-tui/src/tui/terminal.rs @@ -49,6 +49,8 @@ impl Drop for TerminalGuard { } } +/// Reject headless sessions before changing terminal state, then run the TUI +/// and report event-loop failures together with terminal restoration failures. pub(super) fn run(app: &mut App) -> Result<()> { if !is_interactive(io::stdin().is_terminal(), io::stdout().is_terminal()) { bail!( @@ -73,6 +75,8 @@ pub(super) fn is_interactive(stdin_tty: bool, stdout_tty: bool) -> bool { stdin_tty || stdout_tty } +/// Preserve every event-loop and restoration error rather than masking one +/// failure with a later cleanup failure. pub(super) fn finish_terminal( event: Result<()>, raw: Result<()>, diff --git a/bws-tui/src/tui/tests.rs b/bws-tui/src/tui/tests.rs index af7e0a1..82d4e5d 100644 --- a/bws-tui/src/tui/tests.rs +++ b/bws-tui/src/tui/tests.rs @@ -54,6 +54,7 @@ fn edit_fields_cycle_without_invalid_numeric_states() { } #[test] +/// Cover both one-sided terminal sessions and the fully headless rejection. fn tui_gate_accepts_a_tty_on_either_side() { assert!(is_interactive(true, false)); assert!(is_interactive(false, true));