# How gh-stack Handles Non-Interactive Terminals and CI Environments

> gh-stack ensures safe execution in CI by automatically disabling interactive prompts and TUI spinners when stdout is not a terminal. Learn how gh-stack handles non-interactive terminals.

- Repository: [GitHub/gh-stack](https://github.com/github/gh-stack)
- Tags: how-to-guide
- Published: 2026-08-03

---

**gh-stack detects TTY availability through `Config.IsInteractive()`—wrapping the `term` package—to automatically disable prompts, TUI spinners, and interactive workflows when stdout is not a terminal, ensuring safe execution in CI pipelines and scripted automation.**

When automating stacked pull requests in continuous integration environments, command-line tools must avoid blocking for user input. The `github/gh-stack` repository implements a robust terminal detection strategy that gates every interactive feature behind a single configuration method. This design allows the tool to operate seamlessly across local development shells and non-interactive CI runners without hanging or failing.

## Terminal Detection via Config.IsInteractive()

At the core of gh-stack’s adaptability is the `IsInteractive()` method defined in [`internal/config/config.go`](https://github.com/github/gh-stack/blob/main/internal/config/config.go). This function combines runtime terminal detection with a test override flag:

```go
func (c *Config) IsInteractive() bool {
    return c.ForceInteractive || c.Terminal.IsTerminalOutput()
}

```

- **`c.Terminal.IsTerminalOutput()`** returns **true** only when stdout is attached to a TTY, using the `github.com/cli/go-gh/v2/pkg/term` package.
- **`ForceInteractive`** (default **false**) is a test-only boolean that forces interactive mode regardless of the actual terminal state.

Every command that might require user interaction first queries this method. When `IsInteractive()` evaluates to **false**—typical in CI systems, non-TTY shells, or piped commands—the tool follows a non-interactive code path that eliminates all blocking operations.

## Non-Interactive Guard Clauses in Critical Paths

The codebase implements specific guard clauses in utility functions and command handlers to prevent interactive operations when running unattended:

**Git Rerere Prompting (`ensureRerere`)**

The `ensureRerere` function in [`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go) checks the configuration before attempting any user-facing prompts:

```go
if !cfg.IsInteractive() {
    return nil
}

```

This early return makes the call a **no-op** in CI environments, allowing commands to proceed without attempting to configure rerere interactively【/cache/repos/github.com/github/gh-stack/main/cmd/utils.go#L72-L74】.

**Remote Selection (`pickRemote`)**

When multiple remotes exist and the session lacks interactivity, the tool aborts with a descriptive error rather than hanging:

```go
if !cfg.IsInteractive() {
    return "", fmt.Errorf("multiple remotes configured...")
}

```

This prevents CI jobs from stalling indefinitely when the remote cannot be determined automatically【/cache/repos/github.com/github/gh-stack/main/cmd/utils.go#L15-L17】.

**TUI Loading States (`gh stack view`)**

The view command conditionally renders a "Loading stack..." spinner based on terminal capabilities:

```go
if !opts.short && cfg.IsInteractive() {
    // spinner initialization
}

```

In non-interactive runs, the spinner is suppressed to avoid polluting log files with ANSI escape sequences【/cache/repos/github.com/github/gh-stack/main/cmd/view.go#L78-L82】.

**Modify and Submit Workflows**

Commands like `modify` and `submit` check `cfg.IsInteractive()` before entering interactive state machines. When false, these commands exit early or select default behaviors, ensuring that automated scripts never encounter unexpected prompt loops.

## CI-Specific Safety Patterns

Beyond conditional logic, the codebase includes explicit documentation for CI compatibility. A comment in [`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go) notes the design intent for certain utility functions:

```go
// no-op so commands can still run in CI/scripting.

```

This pattern—returning early or selecting safe defaults—appears throughout the `cmd/` package, ensuring that operations requiring user judgment become inert rather than failing catastrophically when stdin is not available【/cache/repos/github.com/github/gh-stack/main/cmd/utils.go#L1358】.

## Implementation Examples

*Detecting terminal state before custom logic:*

```go
cfg := config.New()
if cfg.IsInteractive() {
    // Safe to launch survey prompts or bubbletea TUIs
    result, err := interactivePrompt()
} else {
    // Use environment variables or defaults
    result = os.Getenv("DEFAULT_VALUE")
}

```

*Handling remote selection in scripts:*

```go
remote, err := pickRemote(cfg, currentBranch, "")
if err != nil {
    // In CI: log error and exit with non-zero status
    log.Fatalf("Non-interactive environment: %v", err)
}
// Proceed with identified remote

```

*Forcing interactive mode for testing:*

```go
cfg := config.New()
cfg.ForceInteractive = true // Bypass TTY check in unit tests

```

## Summary

- **Centralized detection** occurs in [`internal/config/config.go`](https://github.com/github/gh-stack/blob/main/internal/config/config.go) via `IsInteractive()`, which queries the `term` package and respects a `ForceInteractive` override.
- **Early returns** in [`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go) functions like `ensureRerere` and `pickRemote` convert interactive operations into no-ops or explicit errors when running in CI.
- **Conditional UI** in commands such as `view` suppresses spinners and TUI elements based on the terminal state.
- **Explicit CI safety** comments document the intentional no-op behavior for scripting environments throughout the codebase.

## Frequently Asked Questions

### How does gh-stack detect if it is running in CI?

gh-stack relies on the `term` package’s `IsTerminalOutput()` method to determine if stdout is attached to a TTY. When this returns false—common in CI containers and piped shells—the `Config.IsInteractive()` method returns false, triggering non-interactive code paths. There is no explicit CI environment variable check; the detection is purely based on terminal capabilities.

### What happens if a command requires user input in a non-interactive shell?

Commands requiring input check `cfg.IsInteractive()` first. If false, they either return immediately (no-op), return an error explaining that interaction is impossible, or select safe defaults. For example, `pickRemote` returns an error when multiple remotes exist and the terminal is non-interactive, while `ensureRerere` simply skips its configuration prompt.

### Can I force interactive mode in tests or scripts?

Yes. The `Config` struct exposes a `ForceInteractive` boolean field that, when set to **true**, causes `IsInteractive()` to return true regardless of the actual terminal state. This is intended primarily for unit testing interactive workflows in headless environments, allowing test suites to exercise prompt logic without a real TTY.

### Which commands are affected by the non-interactive check?

All commands in the `cmd/` directory that might display UI or request input implement this check. Notable examples include `view` (suppresses the loading spinner), `modify` (skips the interactive state machine), `submit` (bypasses confirmation prompts), and utility functions like `ensureRerere` and `pickRemote` in [`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go). Any future command following the repository’s patterns should gate interactive behavior behind `cfg.IsInteractive()`.