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
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, thenrm -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.