#!/usr/bin/env bash # tests/e2e-fix-round-4-5b2-a.sh # End to end for fix round 4.5b2, part A (plan # .claude/projects/2026-10-31-guard-4-5b2-part-a, End-to-end:, decisions 91, # 107 and 123): the Bash hook (hooks/scripts/pre-bash-executor.mjs) reads # commands the way a session sends them, and /bin/bash and /bin/zsh -f show, # each in a temp directory with HOME set to a temp directory, that each block # is real and each pass is text to the shell. # 1. ESR: the three commands that newly pass (a here-string, a closed quoted # heredoc, a closed quote before an escaped <) pass the hook, and both # shells run the line after them as a command (a stub next on PATH # touches ran-next) and create no PWNED file. # 2. Blocks that are no false positive: SFX-X and SFX-dot print # body EXPANDED under both shells; SIS1 and the eval and sh -c forms of a # twice-escaped operator (X1, X1s) create PWNED under both shells; the # [CNT] command expands under /bin/bash, while zsh stops with a parse # error. # 3. The shipped form still passes: the parser block of # commands/trekreview.md (## Phase 1), filled with the repo root and the # typed argument fix "<<" handling. # A missing /bin/zsh is a FAIL, not a skip. # Prints one "ok - " or "FAIL - : " line per case and exits 1 # when any case failed, else 0. Starts nothing but node, /bin/bash and # /bin/zsh (and the usual file tools). bash 3.2 compatible, ASCII only, no # arrays. set -u cd "$(dirname "$0")/.." || { echo "FAIL - setup: cannot cd to the repo root"; exit 1; } ROOT="$(pwd)" HOOK="$ROOT/hooks/scripts/pre-bash-executor.mjs" unset CLAUDE_CODE_SESSION_ID TMP="$(mktemp -d "${TMPDIR:-/tmp}/e2e-fix-round-4-5b2-a.XXXXXX")" || { echo "FAIL - setup: mktemp"; exit 1; } trap 'rm -rf "$TMP"' EXIT TMP="$(cd "$TMP" && pwd -P)" || { echo "FAIL - setup: cannot enter the temp directory"; exit 1; } export LC_ALL=C export LANG=C FAILED=0 pass() { echo "ok - $1"; } bad() { echo "FAIL - $1"; FAILED=$((FAILED + 1)); } # The premises' HOME, and a stub next that touches ran-next in the current # directory. mkdir -p "$TMP/home" "$TMP/bin" || { echo "FAIL - setup: cannot make the temp directories"; exit 1; } printf '%s\n' '#!/bin/sh' 'touch ran-next' > "$TMP/bin/next" && chmod +x "$TMP/bin/next" \ || { echo "FAIL - setup: cannot write the next stub"; exit 1; } # hookrun ...: the Bash hook with a Bash tool input whose command # is the given lines joined by newlines; stderr goes to . Returns the # hook's exit status. hookrun() { out="$1" shift node -e 'process.stdout.write(JSON.stringify({ tool_name: "Bash", tool_input: { command: process.argv.slice(1).join("\n") } }));' "$@" \ | node "$HOOK" > /dev/null 2> "$out" } # hookfile : the Bash hook with the file's text as the command. hookfile() { node -e 'process.stdout.write(JSON.stringify({ tool_name: "Bash", tool_input: { command: require("fs").readFileSync(process.argv[1], "utf8") } }));' "$2" \ | node "$HOOK" > /dev/null 2> "$1" } D=VOYAGE_ARGS_EOF_7F3C9E # blocked ...: the hook exits 2 and names args-heredoc-closed-early. blocked() { bname="$1" bout="$2" shift 2 hookrun "$bout" "$@" c=$? if [ "$c" -ne 2 ]; then bad "$bname: the hook exited $c, not 2: $(cat "$bout")" return 1 elif ! grep -q 'args-heredoc-closed-early' "$bout"; then bad "$bname: the hook did not name args-heredoc-closed-early: $(cat "$bout")" return 1 fi return 0 } # unquoted ...: blocked, with the unquoted reason. unquoted() { qname="$1" uout="$2" shift 2 blocked "$qname" "$uout" "$@" || return 1 if ! grep -q "opens $D unquoted" "$uout"; then bad "$qname: the hook did not give the unquoted reason: $(cat "$uout")" return 1 fi return 0 } # allowed ...: the hook exits 0. allowed() { aname="$1" aout="$2" shift 2 hookrun "$aout" "$@" c=$? if [ "$c" -ne 0 ]; then bad "$aname: the hook exited $c: $(cat "$aout")" return 1 fi return 0 } # premise