Skip to content

feat(core): non-interactive env and PTY skip for Full Access shell exec#27157

Open
emersonbusson wants to merge 2 commits into
google-gemini:mainfrom
emersonbusson:feat/full-access-non-interactive-shell
Open

feat(core): non-interactive env and PTY skip for Full Access shell exec#27157
emersonbusson wants to merge 2 commits into
google-gemini:mainfrom
emersonbusson:feat/full-access-non-interactive-shell

Conversation

@emersonbusson
Copy link
Copy Markdown
Contributor

@emersonbusson emersonbusson commented May 16, 2026

Summary

Two related fixes for Full Access mode (--full-access / --approval-mode=full_access) so shell commands don't hang on interactive sub-prompts:

  1. Inject non-interactive env vars so npm, npx, apt, pip, yarn, git auto-confirm instead of waiting at Ok to proceed? [y].
  2. Route child processes through the stdin-closed child_process path (skip the PTY) so sudo fails fast (a password is required) instead of waiting forever on a PTY password prompt.

Both gated strictly on approvalMode === YOLO — non-Full-Access flows are unchanged.

Details

Env vars (packages/core/src/services/shellExecutionService.ts) — extends the existing non-interactive env block (which only fires in !isInteractive) with a second branch firing on approvalMode === YOLO, regardless of interactive UI. Pre-existing user values pass through via ?? 'default' coalesce. Vars injected:

CI=1
npm_config_yes=true
npm_config_fund=false
npm_config_audit=false
YARN_ENABLE_INTERACTIVE=false
DEBIAN_FRONTEND=noninteractive
NEEDRESTART_MODE=a
GIT_TERMINAL_PROMPT=0
GH_PROMPT_DISABLED=1
GCM_INTERACTIVE=never
PIP_DISABLE_PIP_VERSION_CHECK=1

PTY skip (packages/core/src/tools/shell.ts) — passes shouldUseNodePty = getEnableInteractiveShell() && approvalMode !== YOLO to ShellExecutionService.execute. In Full Access the child_process fallback runs with stdio: ['ignore', 'pipe', 'pipe'], so sudo reads EOF on stdin and exits with sudo: a password is required within a second instead of hanging on the TTY password prompt.

Trade-off accepted: TUI tools like vim, htop, interactive REPLs no longer work inside Full Access. That matches the intent — Full Access is for fire-and-forget execution; interactive TUI sessions don't belong in it. The user can toggle out via Ctrl+Y if they need them.

Prior art

  • Codex CLIcodex-rs/core/src/unified_exec/process_manager.rs:60-71 injects a fixed 10-var env block at every exec (NO_COLOR=1, TERM=dumb, LANG=C.UTF-8, PAGER=cat, CODEX_CI=1, ...). Always on. Codex also calls command.stdin(Stdio::null()) at codex-rs/utils/pty/src/pipe.rs:144 via spawn_process_no_stdin_with_inherited_fds(), which is the same mechanism that makes sudo fail fast.
  • Claude Code (binary v2.1.143) — propagates an allow-list including CI, DEBIAN_FRONTEND, GIT_TERMINAL_PROMPT, NO_COLOR, TERM, COLORTERM, LANG, LC_ALL (from the user's environment to the child process). Also surfaces a detection message when a shell hangs: "The command is likely blocked on an interactive prompt. Kill this task and re-run with piped input (e.g., echo y \| command) or a non-interactive flag if one exists."

This PR is the hybrid: opinionated like Codex (auto-injects, doesn't just propagate), but gated like a user-opt-in feature (only on YOLO).

Related Issues

Closes #24707

Related to (variants of the same hang-on-stdin symptom):

How to Validate

Run gemini --full-access in a fresh terminal, then:

  1. gemini --full-access -p "execute: env | sort | grep -E '^(CI|npm_config|DEBIAN_FRONTEND|NEEDRESTART|GIT_TERMINAL_PROMPT)='" → output includes CI=1, npm_config_yes=true, DEBIAN_FRONTEND=noninteractive, NEEDRESTART_MODE=a, GIT_TERMINAL_PROMPT=0.
  2. gemini --full-access -p "execute: cd /tmp && rm -rf vite-x && npx create-vite@latest vite-x -- --template react" → completes; previously hung at Ok to proceed? [y].
  3. gemini --full-access -p "execute: sudo -n true" → exits within ~1s with sudo: a password is required; previously hung indefinitely on the password prompt.
  4. With CI=0 already exported in the user's shell, run (1) again → output shows CI=0 (the ?? '1' coalesce respects pre-existing user values).
  5. Without --full-access (default mode): the env vars in (1) are NOT present; PTY is used as before.

Pre-Merge Checklist

  • Tests added in shellExecutionService.test.ts: YOLO injects env / DEFAULT does not / user-set CI=0 and DEBIAN_FRONTEND=dialog preserved
  • Tests added in shell.test.ts: PTY enabled when interactive + non-YOLO / PTY skipped in YOLO / PTY skipped when interactiveShell disabled regardless of mode
  • No breaking changes for non-YOLO flows
  • Validated on Linux (Ubuntu 24.04, Node 24.15.0)

Two related fixes for Full Access mode (--full-access / --approval-mode=
full_access) so shell commands don't hang on interactive sub-prompts.

1. Inject non-interactive env vars in shellExecutionService.ts so npm,
   npx, apt, pip, yarn, and git auto-confirm instead of waiting at
   "Ok to proceed? [y]". Pre-existing user values pass through via
   `?? 'default'` coalesce. Vars injected when approvalMode === YOLO:

     CI=1
     npm_config_yes=true
     npm_config_fund=false
     npm_config_audit=false
     YARN_ENABLE_INTERACTIVE=false
     DEBIAN_FRONTEND=noninteractive
     NEEDRESTART_MODE=a
     GIT_TERMINAL_PROMPT=0
     GH_PROMPT_DISABLED=1
     GCM_INTERACTIVE=never
     PIP_DISABLE_PIP_VERSION_CHECK=1

2. Skip the PTY in Full Access (shell.ts passes shouldUseNodePty=false
   when approvalMode===YOLO). The child_process fallback uses
   stdio:['ignore','pipe','pipe'], so sudo reads EOF on stdin and
   exits with "a password is required" within ~1s instead of hanging
   on the TTY password prompt.

Trade-off: TUI tools (vim, htop) no longer work inside Full Access.
That matches the intent — Full Access is fire-and-forget execution;
interactive TUI sessions don't belong in it.

Mirrors prior art: Codex CLI's UNIFIED_EXEC_ENV block at codex-rs/
core/src/unified_exec/process_manager.rs:60-71 (always-on env block)
and command.stdin(Stdio::null()) at codex-rs/utils/pty/src/pipe.rs:144
(stdin closure for fail-fast on sudo). Claude Code propagates a
similar env allow-list from the user's shell. This PR is the hybrid:
opinionated like Codex (auto-injects) but gated like a user opt-in
(YOLO only). Non-YOLO flows are unchanged.

Tests:
  - shellExecutionService.test.ts: YOLO injects env / DEFAULT does not /
    pre-existing user CI=0 and DEBIAN_FRONTEND=dialog preserved
  - shell.test.ts: PTY enabled with interactive + non-YOLO / PTY
    skipped in YOLO / PTY skipped when interactiveShell disabled
@emersonbusson emersonbusson requested review from a team as code owners May 16, 2026 21:07
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces optimizations for the Full Access (YOLO) execution mode to improve reliability and prevent process hangs. By forcing non-interactive behavior in common package managers and CLI tools, and bypassing PTY allocation for child processes, the system now handles fire-and-forget commands more predictably. These changes ensure that interactive prompts do not block execution, while maintaining existing behavior for standard, non-YOLO workflows.

Highlights

  • Non-interactive Environment Injection: Automatically injects a suite of environment variables (e.g., CI=1, DEBIAN_FRONTEND=noninteractive) when in Full Access (YOLO) mode to prevent shell commands from hanging on interactive prompts.
  • PTY Bypass for Full Access: Disables PTY usage during Full Access execution, forcing child processes to use standard pipes. This ensures commands like 'sudo' fail immediately rather than waiting indefinitely for password input.
  • Safety and Preservation: The changes are strictly gated to Full Access mode and use coalescing to ensure that any user-defined environment variables are preserved and take precedence over the injected defaults.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 16, 2026

🛑 Action Required: Evaluation Approval

Steering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged.

Maintainers:

  1. Go to the Workflow Run Summary.
  2. Click the yellow 'Review deployments' button.
  3. Select the 'eval-gate' environment and click 'Approve'.

Once approved, the evaluation results will be posted here automatically.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a 'Full Access' (YOLO) mode for shell execution, which automatically injects non-interactive environment variables (e.g., CI=1, DEBIAN_FRONTEND=noninteractive) to prevent package managers and installers from hanging on stdin prompts. It also modifies the shell tool to skip PTY allocation when in YOLO mode, ensuring that commands like sudo fail fast rather than waiting indefinitely for a password. Feedback was provided regarding the implementation of environment variable injection in shellExecutionService.ts, noting that the current use of nullish coalescing for defaults may be redundant due to existing logic that overwrites certain variables and violates the project's preference for defining defaults within the configuration schema.

Comment on lines +499 to +510
CI: baseEnv['CI'] ?? '1',
npm_config_yes: baseEnv['npm_config_yes'] ?? 'true',
npm_config_fund: baseEnv['npm_config_fund'] ?? 'false',
npm_config_audit: baseEnv['npm_config_audit'] ?? 'false',
YARN_ENABLE_INTERACTIVE: baseEnv['YARN_ENABLE_INTERACTIVE'] ?? 'false',
DEBIAN_FRONTEND: baseEnv['DEBIAN_FRONTEND'] ?? 'noninteractive',
NEEDRESTART_MODE: baseEnv['NEEDRESTART_MODE'] ?? 'a',
GIT_TERMINAL_PROMPT: baseEnv['GIT_TERMINAL_PROMPT'] ?? '0',
GH_PROMPT_DISABLED: baseEnv['GH_PROMPT_DISABLED'] ?? '1',
GCM_INTERACTIVE: baseEnv['GCM_INTERACTIVE'] ?? 'never',
PIP_DISABLE_PIP_VERSION_CHECK:
baseEnv['PIP_DISABLE_PIP_VERSION_CHECK'] ?? '1',
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.

high

In the real application flow (defined in shell.ts), Full Access (YOLO) mode skips the PTY, which means shouldUseNodePty is false. This causes ShellExecutionService.execute to call childProcessFallback with isInteractive = false, which in turn triggers the if (!isInteractive) block at line 458. That block unconditionally overwrites GIT_TERMINAL_PROMPT, GH_PROMPT_DISABLED, and GCM_INTERACTIVE (lines 475-480), making the nullish coalescing in the YOLO block redundant and failing to respect user-provided values for these variables. To ensure user values are preserved as intended, the YOLO block should use sanitizedEnv as the source. Additionally, per repository rules, avoid using nullish coalescing for defaults in the code; these should be defined in the configuration schema to maintain a single source of truth.

Suggested change
CI: baseEnv['CI'] ?? '1',
npm_config_yes: baseEnv['npm_config_yes'] ?? 'true',
npm_config_fund: baseEnv['npm_config_fund'] ?? 'false',
npm_config_audit: baseEnv['npm_config_audit'] ?? 'false',
YARN_ENABLE_INTERACTIVE: baseEnv['YARN_ENABLE_INTERACTIVE'] ?? 'false',
DEBIAN_FRONTEND: baseEnv['DEBIAN_FRONTEND'] ?? 'noninteractive',
NEEDRESTART_MODE: baseEnv['NEEDRESTART_MODE'] ?? 'a',
GIT_TERMINAL_PROMPT: baseEnv['GIT_TERMINAL_PROMPT'] ?? '0',
GH_PROMPT_DISABLED: baseEnv['GH_PROMPT_DISABLED'] ?? '1',
GCM_INTERACTIVE: baseEnv['GCM_INTERACTIVE'] ?? 'never',
PIP_DISABLE_PIP_VERSION_CHECK:
baseEnv['PIP_DISABLE_PIP_VERSION_CHECK'] ?? '1',
CI: sanitizedEnv['CI'],
npm_config_yes: sanitizedEnv['npm_config_yes'],
npm_config_fund: sanitizedEnv['npm_config_fund'],
npm_config_audit: sanitizedEnv['npm_config_audit'],
YARN_ENABLE_INTERACTIVE: sanitizedEnv['YARN_ENABLE_INTERACTIVE'],
DEBIAN_FRONTEND: sanitizedEnv['DEBIAN_FRONTEND'],
NEEDRESTART_MODE: sanitizedEnv['NEEDRESTART_MODE'],
GIT_TERMINAL_PROMPT: sanitizedEnv['GIT_TERMINAL_PROMPT'],
GH_PROMPT_DISABLED: sanitizedEnv['GH_PROMPT_DISABLED'],
GCM_INTERACTIVE: sanitizedEnv['GCM_INTERACTIVE'],
PIP_DISABLE_PIP_VERSION_CHECK:
sanitizedEnv['PIP_DISABLE_PIP_VERSION_CHECK'],
References
  1. Rely on the schema as the single source of truth for configuration defaults, avoiding redundant nullish coalescing operators.
  2. Use a sanitized environment for variable expansion to prevent extensions from bypassing environment variable redaction.

@emersonbusson
Copy link
Copy Markdown
Contributor Author

Related: #27156, #27158 — sibling Full Access UX improvements opened together. Each is independently reviewable and mergeable.

@gemini-cli gemini-cli Bot added the status/need-issue Pull requests that need to have an associated issue. label May 16, 2026
…lock

Per @gemini-code-assist[bot]: in the real production flow, Full Access
(YOLO) forces shouldUseNodePty=false (see shell.ts), so execute() runs
the child_process path with isInteractive=false. The pre-existing
`!isInteractive` block already sets GIT_TERMINAL_PROMPT,
GH_PROMPT_DISABLED, and GCM_INTERACTIVE unconditionally — meaning the
`??` coalesce in the YOLO block for those three was both redundant
AND misleading (it would not actually preserve user values, since the
earlier block overwrites them first).

Remove the three duplicated entries from the YOLO block. The eight
remaining vars (CI, npm_config_yes/fund/audit, YARN_ENABLE_INTERACTIVE,
DEBIAN_FRONTEND, NEEDRESTART_MODE, PIP_DISABLE_PIP_VERSION_CHECK) are
unique to this block; their `??` coalesce continues to respect
pre-existing user values.

Added inline comment documenting the composition with the
`!isInteractive` block so future readers don't re-add the duplicates.

Updated the YOLO test to assert the realistic composed environment
(isInteractive=false, both blocks running) instead of the hypothetical
"YOLO + interactive" case which shell.ts no longer produces.
@emersonbusson
Copy link
Copy Markdown
Contributor Author

Thanks @gemini-code-assist for the careful read.

Pushed 73ab20c46 (no force-push): removed GIT_TERMINAL_PROMPT, GH_PROMPT_DISABLED, and GCM_INTERACTIVE from the YOLO block. You're right — in the real production flow the YOLO path forces shouldUseNodePty=false in shell.ts, so execute() always reaches the !isInteractive block which already sets those three unconditionally. Listing them again in the YOLO block was both redundant and misleading (the ?? would not actually preserve user values).

The remaining 8 vars (CI, npm_config_*, YARN_ENABLE_INTERACTIVE, DEBIAN_FRONTEND, NEEDRESTART_MODE, PIP_DISABLE_PIP_VERSION_CHECK) are unique to the YOLO block; their ?? coalesce continues to work as documented.

Re: defining defaults in the schema instead of ?? — these are behavioral defaults at exec time (they describe how Full Access shapes the child-process environment), not user-facing configuration. Surfacing them as 11 separate settings entries felt like extra surface area for something users are unlikely to want to override individually; happy to follow up with a schema-backed refactor if you'd prefer. Added an inline comment so the composition with !isInteractive is visible to future readers.

Test updated to reflect the realistic composed environment (isInteractive=false, both blocks running).

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Thanks for the update and the clear explanation, @emersonbusson. The removal of the redundant environment variables is the right move, and the logic regarding the composition of the !isInteractive and YOLO blocks is now much clearer. The inline comment is a great addition for future maintainability. The approach to behavioral defaults seems appropriate here, and the updated tests correctly reflect the intended behavior.

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

Labels

area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality priority/p1 Important and should be addressed in the near term.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: run_shell_command hangs for 5 mins on interactive/slow commands (! Shell awaiting input)

1 participant