agent: Fix shell hang on shell syntax errors with terminal tool usage (#59270)

# Objective

When the agent's terminal tool runs a command with an unsupported shell
syntax (e.g. process substitution `<()` in dash), the shell prints an
error and then drops into interactive mode. It shows a `$` prompt and
hangs waiting for input, requiring the user to press Ctrl+D to continue.

This happens because the stdin redirect wraps the command in a subshell
`(...) </dev/null`, which only closes stdin for the inner command. The
outer shell still has its stdin connected to the PTY, so after a syntax
error it reads from the PTY and waits for more input.

## Solution

Replace the subshell wrapping with an `exec`-level redirect. Instead of:

```sh
sh -i -c '(command\n) </dev/null'
```

We now produce:

```sh
sh -i -c 'exec </dev/null; command'
```

`exec </dev/null;` closes stdin for the **entire shell process**. If the
command fails with a syntax error, the shell immediately gets EOF from
stdin and exits cleanly instead of prompting for more input.

The `-i` flag is preserved so that shells which support it still source
their interactive init files and enable job control.

This change applies to `ShellKind::Posix` (sh/bash/dash/zsh) and
`ShellKind::Fish` in both `ShellBuilder::build()` and
`ShellBuilder::build_no_quote()`.

## Testing

- Manually verified that `cat <(echo hi)` (process substitution in dash)
no longer hangs: the shell exits immediately with the syntax error

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed agent terminal tool hanging with a `$` prompt when commands
encounter syntax errors or unsupported shell features like process
substitution
This commit is contained in:
procr1337 authored and GitHub committed 2026-06-15 07:03:24 +00:00
1 parent ae84331f5a
commit b600f44d69
1 file changed
+8 -12
+8 -12
View File
@@ -99,12 +99,10 @@ impl ShellBuilder {
});
if self.redirect_stdin {
match self.kind {
ShellKind::Fish => {
combined_command.insert_str(0, "begin; ");
combined_command.push_str("; end </dev/null");
ShellKind::Fish | ShellKind::Posix => {
combined_command.insert_str(0, "exec </dev/null; ");
}
ShellKind::Posix
| ShellKind::Nushell
ShellKind::Nushell
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -145,12 +143,10 @@ impl ShellBuilder {
});
if self.redirect_stdin {
match self.kind {
ShellKind::Fish => {
combined_command.insert_str(0, "begin; ");
combined_command.push_str("; end </dev/null");
ShellKind::Fish | ShellKind::Posix => {
combined_command.insert_str(0, "exec </dev/null; ");
}
ShellKind::Posix
| ShellKind::Nushell
ShellKind::Nushell
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -286,7 +282,7 @@ mod test {
.build(Some("echo".into()), &["test".to_string()]);
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "begin; echo test; end </dev/null"]);
assert_eq!(args, vec!["-i", "-c", "exec </dev/null; echo test"]);
}
#[test]
@@ -302,7 +298,7 @@ mod test {
assert_eq!(program, "sh");
assert_eq!(
args,
vec!["-i", "-c", "(cat <<EOF\nhello\nEOF\n) </dev/null"]
vec!["-i", "-c", "exec </dev/null; cat <<EOF\nhello\nEOF"]
);
}