Bash tool: `set -e` is structurally inert -- command runs as `eval` in a non-final `&&` list member, so errexit is suppressed for the whole script

Status Open
Reported on v2.1.251
Maintainer reply None cached
Activity 0 comments · opened Aug 29, 2026

Environment

Claude Code 2.1.251

Describe the bug

set -e has no effect inside a Bash tool command: execution continues past a
failing line, in the main script and in subshells. A model (or user) that
writes a defensive set -e script gets no fail-fast behavior, and if the
script ends with a successful command the whole tool call reports exit 0 --
the failure is invisible to the model.

Repro

Ask Claude to run this as a single Bash tool command:

set -e
false
echo survived

Output: survived (tool call succeeds). Expected: abort after false, no
output, nonzero exit. The subshell form (set -e; false; echo survived)
also prints survived.

Root cause

ps -o args -p $$ from inside a tool call shows the wrapper:

bash -c "source <snapshot>.sh 2>/dev/null || true && shopt -u extglob ... && eval '<user command>' < /dev/null && pwd -P >| /tmp/claude-<n>-cwd"

The user command runs inside eval '...', which is a NON-final member of a
&& list. Per POSIX / bash semantics, errexit is suppressed for every
command executing in that context, including subshells -- set -e inside the
eval body can never take effect. (This is the documented bash behavior: "The
shell does not exit if the command that fails is part of the command list
immediately following a while or until keyword, part of the test in an if
statement, part of any command executed in a && or || list except the command
following the final && or || ...".)

Impact

Real incident: a copy-then-cleanup script (set -e, several cp lines, then
rm -rf of the sources) had the cp lines fail; set -e did not stop the
script, the rm -rf ran, and the sources were deleted before any copy had
landed. The tool call still exited 0 because the last line succeeded.

Workarounds

bash -ec '...' as the command works (fresh bash, own errexit context), as
does &&-chaining lines. Neither is discoverable: nothing tells the model
that set -e was silently ignored.

Suggested fix

Restructure the wrapper so the eval'd user command is not in a non-final
&&/|| position -- e.g. run it as the final list member and capture cwd
separately, or eval '<cmd>'; rc=$?; pwd -P >| ...; exit $rc. If the wrapper
cannot change, documenting that set -e does not work in the Bash tool would
at least let models/users reach for bash -ec.

View original on GitHub ↗