Skip to content

fix(core): detect zsh from $SHELL to prevent shopt errors#26912

Open
kimjune01 wants to merge 2 commits into
google-gemini:mainfrom
kimjune01:fix/26754-zsh-shopt
Open

fix(core): detect zsh from $SHELL to prevent shopt errors#26912
kimjune01 wants to merge 2 commits into
google-gemini:mainfrom
kimjune01:fix/26754-zsh-shopt

Conversation

@kimjune01
Copy link
Copy Markdown
Contributor

Summary

  • Reads $SHELL env var in getShellConfiguration() to detect the user's actual shell instead of hardcoding bash
  • Adds zsh to the ShellType union so zsh users get their shell invoked directly (prevents shopt errors when bash is not the default shell)
  • Reuses the bash parser for zsh command parsing since the syntax overlap is sufficient
  • Updates hasRedirection to handle zsh alongside bash

Fixes #26754

Test plan

  • Added test: "should return zsh configuration when SHELL is zsh"
  • Added test: "should fall back to bash when SHELL is not set"
  • Updated existing tests to mock process.env.SHELL for deterministic behavior
  • All shell-utils tests pass (65 passed, 1 skipped)
  • All shell.test.ts tests pass (78 passed)

kimjune01 added 2 commits May 9, 2026 16:24
On macOS (where zsh is the default shell), run_shell_command fails
because getShellConfiguration() hardcodes 'bash' for all Unix systems.
This causes ensurePromptvarsDisabled() to inject 'shopt -u ...' which
is bash-only and fails in zsh with "command not found: shopt".

Changes:
- Add 'zsh' to ShellType union
- Update getShellConfiguration() to detect shell from process.env.SHELL
- Parse shell basename to set correct ShellType ('bash' or 'zsh')
- Update parseCommandDetails() and hasRedirection() to handle zsh
  using the bash parser (syntax is compatible)
- ensurePromptvarsDisabled() already guards on shell === 'bash',
  so no zsh commands will receive the shopt guard

Fixes google-gemini#26754
The implementation now reads $SHELL to detect zsh vs bash, but the
tests were not mocking it. On zsh hosts (macOS default), the Linux
and macOS tests fail because they expect hardcoded 'bash' but get
'/bin/zsh'. Mock process.env.SHELL explicitly and add dedicated zsh
and unset-SHELL test cases.
@kimjune01 kimjune01 requested a review from a team as a code owner May 12, 2026 09:01
@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 improves shell environment detection and compatibility by allowing the system to identify and utilize the user's configured shell (specifically zsh) rather than assuming bash. By enabling zsh support and reusing existing bash parsing logic, the changes resolve issues where shell-specific commands like shopt caused errors for zsh users.

Highlights

  • Shell Detection: Updated getShellConfiguration to dynamically detect the user's shell using the SHELL environment variable instead of defaulting to bash.
  • Zsh Support: Added zsh to the ShellType definition and configured the system to reuse the bash parser for zsh, ensuring compatibility and preventing shopt errors.
  • Redirection Handling: Updated hasRedirection to include zsh, ensuring consistent command parsing behavior across both bash and zsh environments.
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.

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 shell detection logic to support zsh alongside bash on Unix-like systems, updating command parsing and redirection checks accordingly. However, using the bash parser for zsh poses a security risk as it may fail to identify nested commands in zsh-specific syntax, potentially bypassing access controls. There is also a compatibility concern where unknown shells are incorrectly identified as bash, which could lead to execution failures; a fallback to a known shell is recommended. Finally, the new unit tests violate the repository style guide by directly modifying process.env instead of using vi.stubEnv.

Comment on lines +634 to 637
if (configuration.shell === 'bash' || configuration.shell === 'zsh') {
// Use bash parser for both bash and zsh (syntax is similar enough)
return parseBashCommandDetails(command);
}
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.

security-high high

The parseCommandDetails function uses the bash parser (parseBashCommandDetails) for both bash and zsh. While the syntax is similar, zsh has unique features like =(command) process substitution that the bash parser does not recognize as a command execution. This leads to a bypass of the command restriction policy (IDOR/Broken Access Control) because getCommandRoots will fail to identify and validate nested commands executed via zsh-specific syntax.

Comment on lines +677 to +696
const shellEnv = process.env['SHELL'];
const executable = shellEnv || 'bash';

// Determine shell type from the executable path
let shellType: ShellType = 'bash';
if (shellEnv) {
const basename = path.basename(shellEnv);
if (basename.includes('zsh')) {
shellType = 'zsh';
} else if (basename.includes('bash')) {
shellType = 'bash';
}
// Default to bash for unknown shells to maintain compatibility
}

return {
executable,
argsPrefix: ['-c'],
shell: shellType,
};
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

The current implementation introduces a regression for users with non-bash/non-zsh shells (e.g., sh, dash, or fish). If $SHELL is set to an unknown shell, the code uses it as the executable but identifies the shellType as bash. This causes the execution service to inject bash-specific shopt commands, which will result in errors in those shells. To maintain compatibility and prevent execution failures, the tool should only use the environment's shell if it is explicitly recognized as bash or zsh, otherwise it should fall back to the standard bash executable. Additionally, ensure that the optional SHELL environment variable is handled correctly by coding against the interface contract.

  const shellEnv = process.env['SHELL'];

  if (shellEnv) {
    const basename = path.basename(shellEnv);
    if (basename.includes('zsh')) {
      return { executable: shellEnv, argsPrefix: ['-c'], shell: 'zsh' };
    }
    if (basename.includes('bash')) {
      return { executable: shellEnv, argsPrefix: ['-c'], shell: 'bash' };
    }
  }

  // Fallback to bash for unknown shells or if SHELL is not set to maintain compatibility
  return {
    executable: 'bash',
    argsPrefix: ['-c'],
    shell: 'bash',
  };
References
  1. When consuming an object, if a property is optional in its type definition (interface), callers must handle the undefined case (e.g., by providing a default with ??). Code against the interface contract.

@gemini-cli gemini-cli Bot added priority/p2 Important but can be addressed in a future release. area/core Issues related to User Interface, OS Support, Core Functionality labels May 12, 2026
@kimjune01
Copy link
Copy Markdown
Contributor Author

Investigated the security concern. The bash parser reuse for zsh does not create a net-new bypass:

zsh-specific syntax (=(), ${(flags)}, anonymous functions): The tree-sitter bash parser sets hasError: true for these constructs, which triggers the fail-safe path in commandSafety.ts: ASK_USER in interactive mode, DENY in non-interactive. Tested with =(whoami), ${(s.:.)PATH}, () { rm -rf / } -- all correctly flagged.

Precommand modifiers (noglob, nocorrect): These parse cleanly but are misidentified as the root command, hiding the real executable. However:

  • They are not on any allowlist, so they trigger ASK_USER (not silent execution)
  • The full command string is always shown in the confirmation prompt
  • This same gap already exists for bash builtins: env rm, nice rm, command rm, time rm all hide the real executable behind a prefix the parser treats as the command. This is a pre-existing architectural limitation of the command extraction logic, not a regression from this PR.

The actual risk is a policy downgrade: a DENY rule for rm could be bypassed to ASK_USER via noglob rm. But this is identical to env rm in bash today. The fix is precommand-modifier stripping in parseBashCommandDetails, which should be a follow-up issue covering both bash and zsh prefixes.

Will fix the two non-security items: unknown shell fallback (fish/dash to bash executable) and test style (vi.stubEnv).

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

Labels

area/core Issues related to User Interface, OS Support, Core Functionality priority/p2 Important but can be addressed in a future release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: run_shell_command fails on macOS (zsh) due to hardcoded shopt guard

1 participant