#!/usr/bin/env bash
#
# PreToolUse Hook: Validate Bash commands for RLCR loop
#
# Blocks attempts to bypass Write/Edit hooks using shell commands:
# - cat/echo/printf > file.md (redirection)
# - tee file.md
# - sed -i file.md (in-place edit)
# - goal-tracker.md modifications via Bash
#

set -euo pipefail

# Load shared functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$SCRIPT_DIR/lib/loop-common.sh"

# ========================================
# Parse Hook Input
# ========================================

HOOK_INPUT=$(cat)

# Validate JSON input structure
if ! validate_hook_input "$HOOK_INPUT"; then
    exit 1
fi

# Check for deeply nested JSON (potential DoS)
if is_deeply_nested "$HOOK_INPUT" 30; then
    exit 1
fi

TOOL_NAME="$VALIDATED_TOOL_NAME"

if [[ "$TOOL_NAME" != "Bash" ]]; then
    exit 0
fi

# Require command for Bash tool
if ! require_tool_input_field "$HOOK_INPUT" "command"; then
    exit 1
fi

COMMAND=$(echo "$HOOK_INPUT" | jq -r '.tool_input.command // ""')
COMMAND_LOWER=$(to_lower "$COMMAND")

# ========================================
# Find Active Loops (needed for multiple checks)
# ========================================

PROJECT_ROOT="$(resolve_project_root)" || exit 0

# Extract session_id from hook input for session-aware loop filtering
HOOK_SESSION_ID=$(extract_session_id "$HOOK_INPUT")

# Check for active RLCR loop (filtered by session_id)
LOOP_BASE_DIR="$PROJECT_ROOT/.humanize/rlcr"
ACTIVE_LOOP_DIR=$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID")

# ========================================
# Methodology Analysis Phase Bash Restriction
# ========================================
# During methodology analysis, block file-modifying bash commands.
# Only read-only operations and cancel-rlcr-loop.sh are allowed.
# This prevents source code modifications after Codex has signed off.
#
# Accepted limitations:
# - Read-only bash commands (cat, grep, find, etc.) are NOT blocked. Blocking
#   them would break basic Claude operations. The analysis prompt directs Claude
#   to derive user-facing content only from methodology-analysis-report.md.
# - Spawned agents (different session_id) are not restricted by hooks; their
#   sanitization is enforced by the analysis prompt. This is an inherent
#   limitation of the hook architecture which cannot distinguish spawned agents
#   from unrelated sessions.
#
# Use only the session-matched loop. Do NOT fall back to an unfiltered search,
# as that would incorrectly restrict unrelated sessions opened in the same repo.
_MA_BASH_DIR="$ACTIVE_LOOP_DIR"

if [[ -n "$_MA_BASH_DIR" ]] && [[ -f "$_MA_BASH_DIR/methodology-analysis-state.md" ]]; then
    # Allow cancel-rlcr-loop.sh only as the leading command (not as an argument
    # to another command like cp/mv). The optional path prefix must be a single
    # token with no embedded whitespace, otherwise commands like
    # `bash cancel-rlcr-loop.sh` or `tee cancel-rlcr-loop.sh` would match.
    # The script name must be followed by whitespace or end-of-line so trailing
    # tokens cannot hide additional arguments.
    #
    # Also reject any shell metacharacter that can inject or redirect work
    # after the cancel invocation: pipes/sequence/background operators,
    # command substitution ($(...) or backticks), redirection (<, >), and
    # multi-line payloads. The earlier narrower check only rejected ; | &,
    # letting payloads like `cancel-rlcr-loop.sh $(touch /tmp/pwn)` or a
    # newline-delimited second command slip past this early exit and reach
    # arbitrary file modifications before the downstream blockers run.
    _ma_has_shell_meta=false
    case "$COMMAND_LOWER" in
        *';'*|*'|'*|*'&'*|*'`'*|*'>'*|*'<'*|*'$('*|*$'\n'*)
            _ma_has_shell_meta=true
            ;;
    esac
    if [[ "$_ma_has_shell_meta" != "true" ]] && \
       echo "$COMMAND_LOWER" | grep -qE '^[[:space:]]*"?([^[:space:]"]+/)?cancel-rlcr-loop\.sh"?([[:space:]]|$)'; then
        exit 0
    fi
    # Block git commands that modify the working tree
    if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])git[[:space:]]+(commit|add|reset|checkout|merge|rebase|cherry-pick|am|apply|stash|push|restore|clean|rm|mv|switch|pull|clone|submodule|worktree)'; then
        echo "# Bash Blocked During Methodology Analysis

Git write commands are not allowed during the methodology analysis phase." >&2
        exit 2
    fi
    # Block file manipulation commands (touch, mv, cp, rm, mkdir, ln, patch, etc.)
    if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])(tee|install|touch|mv|cp|rm|dd|truncate|chmod|chown|mkdir|rmdir|ln|mktemp|patch)[[:space:]]'; then
        echo "# Bash Blocked During Methodology Analysis

File modification commands are not allowed during the methodology analysis phase." >&2
        exit 2
    fi
    # Block in-place file editing tools
    if echo "$COMMAND_LOWER" | grep -qE 'sed[[:space:]]+-i|awk[[:space:]]+-i[[:space:]]+inplace|perl[[:space:]]+-[^[:space:]]*i'; then
        echo "# Bash Blocked During Methodology Analysis

In-place file editing is not allowed during the methodology analysis phase." >&2
        exit 2
    fi
    # Block common interpreters that could write files (defense-in-depth)
    if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])(python[23]?|ruby|node|perl|php)[[:space:]]'; then
        echo "# Bash Blocked During Methodology Analysis

Running interpreters is not allowed during the methodology analysis phase." >&2
        exit 2
    fi
    # Block shell script entry points (bash script.sh, sh script.sh, source, .)
    if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])(/usr/bin/env[[:space:]]+)?(bash|sh|zsh|/bin/bash|/bin/sh|/bin/zsh)[[:space:]]'; then
        echo "# Bash Blocked During Methodology Analysis

Running shell scripts is not allowed during the methodology analysis phase." >&2
        exit 2
    fi
    # Block build tools that execute arbitrary commands
    if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])(make|cmake|ninja|gradle|mvn|ant|cargo|go[[:space:]]+run|go[[:space:]]+generate|npm[[:space:]]+run|yarn[[:space:]]+run|npx|pnpm)[[:space:]]'; then
        echo "# Bash Blocked During Methodology Analysis

Build tools are not allowed during the methodology analysis phase." >&2
        exit 2
    fi
    # Block source/dot commands (source script.sh, . script.sh)
    if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])(source|\.)[ 	]+[^[:space:]]'; then
        echo "# Bash Blocked During Methodology Analysis

Sourcing scripts is not allowed during the methodology analysis phase." >&2
        exit 2
    fi
    # Block direct script execution (./script.sh, ../script.sh, /path/to/script)
    if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])\.{0,2}/[^[:space:]>|&;]*\.(sh|bash|py|rb|pl|js)'; then
        echo "# Bash Blocked During Methodology Analysis

Direct script execution is not allowed during the methodology analysis phase." >&2
        exit 2
    fi
    # Block output redirection to files (catches cat > file, echo > file, etc.)
    # Strip safe redirections (/dev/ paths, fd duplication) then check for remaining >
    _ma_stripped=$(echo "$COMMAND_LOWER" | sed 's|[0-9]*>[>]*[[:space:]]*/dev/[^[:space:]]*||g; s|[0-9]*>&[0-9]*||g')
    if echo "$_ma_stripped" | grep -qE '[>]'; then
        echo "# Bash Blocked During Methodology Analysis

File redirection is not allowed during the methodology analysis phase." >&2
        exit 2
    fi
fi

# If no active RLCR loop, allow all commands
if [[ -z "$ACTIVE_LOOP_DIR" ]]; then
    exit 0
fi

# ========================================
# Block Direct Execution of Hook Scripts
# ========================================
# Prevents Claude from manually running stop hook or stop gate scripts.
# These scripts should only be invoked by the hooks system, not via Bash.

BLOCKED_HOOK_SCRIPTS="(loop-codex-stop-hook\.sh|rlcr-stop-gate\.sh)"
HOOK_ASSIGNMENT_PREFIX="[[:alpha:]_][[:alnum:]_]*=[^[:space:];&|]+"
HOOK_COMMAND_PREFIX="command([[:space:]]+(-[^[:space:];&|]+|--))*"
HOOK_ENV_PREFIX="env([[:space:]]+(-[^[:space:];&|]+|--|${HOOK_ASSIGNMENT_PREFIX}))*"
HOOK_UTILITY_ARG="[^[:space:];&|]+"
HOOK_TIMEOUT_OPTION="(-[^[:space:];&|]+([[:space:]]+${HOOK_UTILITY_ARG})?|--([^[:space:];&|]+(=${HOOK_UTILITY_ARG}|[[:space:]]+${HOOK_UTILITY_ARG})?)?)"
HOOK_NICE_OPTION="(-n([[:space:]]+${HOOK_UTILITY_ARG})?|--adjustment(=${HOOK_UTILITY_ARG}|[[:space:]]+${HOOK_UTILITY_ARG})|-[^[:space:];&|]+|--[^[:space:];&|]+)"
HOOK_TRACE_OPTION="(-[^[:space:];&|]+([[:space:]]+${HOOK_UTILITY_ARG})?|--([^[:space:];&|]+(=${HOOK_UTILITY_ARG}|[[:space:]]+${HOOK_UTILITY_ARG})?)?)"
HOOK_TIMEOUT_PREFIX="timeout([[:space:]]+(${HOOK_TIMEOUT_OPTION}))*([[:space:]]+--)?[[:space:]]+${HOOK_UTILITY_ARG}"
HOOK_NICE_PREFIX="nice([[:space:]]+(${HOOK_NICE_OPTION}))*([[:space:]]+--)?"
HOOK_NOHUP_PREFIX="nohup"
HOOK_TRACE_PREFIX="(strace|ltrace)([[:space:]]+(${HOOK_TRACE_OPTION}))*([[:space:]]+--)?"
HOOK_UTILITY_PREFIX="(${HOOK_TIMEOUT_PREFIX}|${HOOK_NICE_PREFIX}|${HOOK_NOHUP_PREFIX}|${HOOK_TRACE_PREFIX})"
HOOK_WRAPPER_PREFIX_PATTERN="((${HOOK_ASSIGNMENT_PREFIX}|${HOOK_COMMAND_PREFIX}|${HOOK_ENV_PREFIX}|${HOOK_UTILITY_PREFIX})[[:space:]]+)*"
HOOK_LAUNCH_PATTERN="(([^[:space:]]*/)?|(bash|sh|zsh|source|\.)[[:space:]].*)$BLOCKED_HOOK_SCRIPTS"
if echo "$COMMAND_LOWER" | grep -qE "(^|[;&|])[[:space:]]*${HOOK_WRAPPER_PREFIX_PATTERN}${HOOK_LAUNCH_PATTERN}"; then
    stop_hook_direct_execution_blocked_message >&2
    exit 2
fi

# ========================================
# RLCR Loop Specific Checks
# ========================================
# The following checks only apply when an RLCR loop is active

if [[ -n "$ACTIVE_LOOP_DIR" ]]; then
    # Detect if we're in Finalize Phase (finalize-state.md exists)
    STATE_FILE=$(resolve_active_state_file "$ACTIVE_LOOP_DIR")

    # Parse state file using strict validation (fail closed on malformed state)
    if ! parse_state_file_strict "$STATE_FILE" 2>/dev/null; then
        echo "Error: Malformed state file, blocking operation for safety" >&2
        exit 1
    fi
    CURRENT_ROUND="$STATE_CURRENT_ROUND"

    # ========================================
    # Block Git Push When push_every_round is false
    # ========================================
    # Default behavior: commits stay local, no need to push to remote

    # Note: parse_state_file was called above, STATE_* vars are available
    PUSH_EVERY_ROUND="$STATE_PUSH_EVERY_ROUND"

    if [[ "$PUSH_EVERY_ROUND" != "true" ]]; then
        # Check if command is a git push command
        if [[ "$COMMAND_LOWER" =~ ^[[:space:]]*git[[:space:]]+push ]]; then
            FALLBACK="# Git Push Blocked

Commits should stay local during the RLCR loop.
Use --push-every-round flag when starting the loop if you need to push each round."
            load_and_render_safe "$TEMPLATE_DIR" "block/git-push.md" "$FALLBACK" >&2
            exit 2
        fi
    fi
fi

# ========================================
# Block Git Add Commands Targeting .humanize
# ========================================
# Prevents force-adding .humanize files to version control
# Note: .humanize is in .gitignore, but git add -f bypasses it

if git_adds_humanize "$COMMAND_LOWER"; then
    git_add_humanize_blocked_message >&2
    exit 2
fi

# ========================================
# RLCR State and File Protection
# ========================================
# These checks only apply when an RLCR loop is active

if [[ -n "$ACTIVE_LOOP_DIR" ]]; then

# ========================================
# Block State File Modifications (All Rounds)
# ========================================
# State file is managed by the loop system, not Claude
# This includes both state.md and finalize-state.md
# NOTE: Check finalize-state.md FIRST because state\.md pattern also matches finalize-state.md
# Exception: Allow mv to cancel-state.md when cancel signal file exists
#
# Note: We check TWO patterns for mv/cp:
# 1. command_modifies_file checks if DESTINATION contains state.md
# 2. Additional check below catches if SOURCE contains state.md (e.g., mv state.md /tmp/foo)

if command_modifies_file "$COMMAND_LOWER" "methodology-analysis-state\.md"; then
    # Check for cancel signal file - allow authorized cancel operation
    if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
        exit 0
    fi
    methodology_analysis_state_file_blocked_message >&2
    exit 2
fi

if command_modifies_file "$COMMAND_LOWER" "finalize-state\.md"; then
    # Check for cancel signal file - allow authorized cancel operation
    if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
        exit 0
    fi
    finalize_state_file_blocked_message >&2
    exit 2
fi

# Check 1: Destination contains state.md (covers writes, redirects, mv/cp TO state.md)
if command_modifies_file "$COMMAND_LOWER" "state\.md"; then
    # Check for cancel signal file - allow authorized cancel operation
    if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
        exit 0
    fi
    state_file_blocked_message >&2
    exit 2
fi

# Check 2: Source of mv/cp contains state.md (covers mv/cp FROM state.md to any destination)
# This catches bypass attempts like: mv state.md /tmp/foo.txt
# Pattern handles:
# - Options like -f, -- before the source path
# - Leading whitespace and command prefixes with options (sudo -u root, env VAR=val, command --)
# - Quoted relative paths like: mv -- "state.md" /tmp/foo
# - Command chaining via ;, &&, ||, |, |&, & (each segment is checked independently)
# - Shell wrappers: sh -c, bash -c, /bin/sh -c, /bin/bash -c
# Requires state.md to be a proper filename (preceded by space, /, or quote)
# Note: sudo/command patterns match zero or more arguments (each: space + optional-minus + non-space chars)

# Split command on shell operators and check each segment
# This catches chained commands like: true; mv state.md /tmp/foo
MV_CP_SOURCE_PATTERN="^[[:space:]]*(sudo([[:space:]]+-?[^[:space:];&|]+)*[[:space:]]+)?(env[[:space:]]+[^;&|]*[[:space:]]+)?(command([[:space:]]+-?[^[:space:];&|]+)*[[:space:]]+)?(mv|cp)[[:space:]].*[[:space:]/\"']state\.md"
MV_CP_FINALIZE_SOURCE_PATTERN="^[[:space:]]*(sudo([[:space:]]+-?[^[:space:];&|]+)*[[:space:]]+)?(env[[:space:]]+[^;&|]*[[:space:]]+)?(command([[:space:]]+-?[^[:space:];&|]+)*[[:space:]]+)?(mv|cp)[[:space:]].*[[:space:]/\"']finalize-state\.md"
MV_CP_METHODOLOGY_SOURCE_PATTERN="^[[:space:]]*(sudo([[:space:]]+-?[^[:space:];&|]+)*[[:space:]]+)?(env[[:space:]]+[^;&|]*[[:space:]]+)?(command([[:space:]]+-?[^[:space:];&|]+)*[[:space:]]+)?(mv|cp)[[:space:]].*[[:space:]/\"']methodology-analysis-state\.md"

# Replace shell operators with newlines, then check each segment
# Order matters: |& before |, && before single &
# For &: protect redirections (&>>, &>, >&, N>&M) with placeholders, then split on remaining &
# Placeholders use control chars unlikely to appear in commands
# Note: &>> must be replaced before &> to avoid leaving a stray >
COMMAND_SEGMENTS=$(echo "$COMMAND_LOWER" | sed '
    s/|&/\n/g
    s/&&/\n/g
    s/&>>/\x03/g
    s/&>/\x01/g
    s/[0-9]*>&[0-9]*/\x02/g
    s/>&/\x02/g
    s/&/\n/g
    s/||/\n/g
    s/|/\n/g
    s/;/\n/g
')
while IFS= read -r SEGMENT; do
    # Skip empty segments
    [[ -z "$SEGMENT" ]] && continue

    # Strip leading redirections before pattern matching
    # This handles cases like: 2>/tmp/x mv, 2> /tmp/x mv, >/tmp/x mv, 2>&1 mv, &>/tmp/x mv
    # Also handles append redirections: >> /tmp/x mv, 2>> /tmp/x mv, &>> /tmp/x mv
    # Also handles quoted targets: >> "/tmp/x y" mv, >> '/tmp/x y' mv
    # Also handles ANSI-C quoting: >> $'/tmp/x y' mv, >> $"/tmp/x y" mv
    # Also handles escaped-space targets: >> /tmp/x\ y mv
    # Must handle:
    # - \x01 (from &>) followed by optional space and target path (quoted, ANSI-C, escaped, or unquoted)
    # - \x02 (from >&, 2>&1) with NO target - just strip placeholder
    # - \x03 (from &>>) followed by optional space and target path (quoted, ANSI-C, escaped, or unquoted)
    # - Standard redirections [0-9]*[><]+ followed by optional space and target
    # Order: double-quoted, single-quoted, ANSI-C $'...', locale $"...", escaped-unquoted, plain-unquoted
    # Note: Escaped/ANSI-C patterns use sed -E for extended regex
    SEGMENT_CLEANED=$(echo "$SEGMENT" | sed '
        :again
        s/^[[:space:]]*\x01[[:space:]]*"[^"]*"[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*\x01[[:space:]]*'"'"'[^'"'"']*'"'"'[[:space:]]*//
        t again
    ' | sed -E "
        :again
        s/^[[:space:]]*\x01[[:space:]]*\\$'([^'\\\\]|\\\\.)*'[[:space:]]*//
        t again
    " | sed -E '
        :again
        s/^[[:space:]]*\x01[[:space:]]*\$"([^"\\]|\\.)*"[[:space:]]*//
        t again
    ' | sed -E '
        :again
        s/^[[:space:]]*\x01[[:space:]]*([^[:space:]\\]|\\.)+[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*\x01[[:space:]]*[^[:space:]]*[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*\x02[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*\x03[[:space:]]*"[^"]*"[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*\x03[[:space:]]*'"'"'[^'"'"']*'"'"'[[:space:]]*//
        t again
    ' | sed -E "
        :again
        s/^[[:space:]]*\x03[[:space:]]*\\$'([^'\\\\]|\\\\.)*'[[:space:]]*//
        t again
    " | sed -E '
        :again
        s/^[[:space:]]*\x03[[:space:]]*\$"([^"\\]|\\.)*"[[:space:]]*//
        t again
    ' | sed -E '
        :again
        s/^[[:space:]]*\x03[[:space:]]*([^[:space:]\\]|\\.)+[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*\x03[[:space:]]*[^[:space:]]*[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*[0-9]*[><][><]*[[:space:]]*"[^"]*"[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*[0-9]*[><][><]*[[:space:]]*'"'"'[^'"'"']*'"'"'[[:space:]]*//
        t again
    ' | sed -E "
        :again
        s/^[[:space:]]*[0-9]*[><]+[[:space:]]*\\$'([^'\\\\]|\\\\.)*'[[:space:]]*//
        t again
    " | sed -E '
        :again
        s/^[[:space:]]*[0-9]*[><]+[[:space:]]*\$"([^"\\]|\\.)*"[[:space:]]*//
        t again
    ' | sed -E '
        :again
        s/^[[:space:]]*[0-9]*[><]+[[:space:]]*([^[:space:]\\]|\\.)+[[:space:]]*//
        t again
    ' | sed '
        :again
        s/^[[:space:]]*[0-9]*[><][><]*[[:space:]]*[^[:space:]]*[[:space:]]*//
        t again
    ')

    # Check for methodology-analysis-state.md as SOURCE first (most specific pattern)
    if echo "$SEGMENT_CLEANED" | grep -qE "$MV_CP_METHODOLOGY_SOURCE_PATTERN"; then
        # Check for cancel signal file - allow authorized cancel operation
        if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
            exit 0
        fi
        methodology_analysis_state_file_blocked_message >&2
        exit 2
    fi

    # Check for finalize-state.md as SOURCE (more specific than state.md)
    if echo "$SEGMENT_CLEANED" | grep -qE "$MV_CP_FINALIZE_SOURCE_PATTERN"; then
        # Check for cancel signal file - allow authorized cancel operation
        if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
            exit 0
        fi
        finalize_state_file_blocked_message >&2
        exit 2
    fi

    if echo "$SEGMENT_CLEANED" | grep -qE "$MV_CP_SOURCE_PATTERN"; then
        # Check for cancel signal file - allow authorized cancel operation
        if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
            exit 0
        fi
        state_file_blocked_message >&2
        exit 2
    fi
done <<< "$COMMAND_SEGMENTS"

# Check 3: Shell wrapper bypass (sh -c, bash -c)
# This catches bypass attempts like: sh -c 'mv state.md /tmp/foo'
# Pattern: look for sh/bash with -c flag and state.md or finalize-state.md in the payload
if echo "$COMMAND_LOWER" | grep -qE "(^|[[:space:]/])(sh|bash)[[:space:]]+-c[[:space:]]"; then
    # Shell wrapper detected - check if payload contains mv/cp methodology-analysis-state.md (most specific)
    if echo "$COMMAND_LOWER" | grep -qE "(mv|cp)[[:space:]].*methodology-analysis-state\.md"; then
        if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
            exit 0
        fi
        methodology_analysis_state_file_blocked_message >&2
        exit 2
    fi
    # Shell wrapper detected - check if payload contains mv/cp finalize-state.md (check first, more specific)
    if echo "$COMMAND_LOWER" | grep -qE "(mv|cp)[[:space:]].*finalize-state\.md"; then
        # Check for cancel signal file - allow authorized cancel operation
        if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
            exit 0
        fi
        finalize_state_file_blocked_message >&2
        exit 2
    fi
    # Shell wrapper detected - check if payload contains mv/cp state.md
    if echo "$COMMAND_LOWER" | grep -qE "(mv|cp)[[:space:]].*state\.md"; then
        # Check for cancel signal file - allow authorized cancel operation
        if is_cancel_authorized "$ACTIVE_LOOP_DIR" "$COMMAND_LOWER"; then
            exit 0
        fi
        state_file_blocked_message >&2
        exit 2
    fi
fi

# ========================================
# Block Plan Backup Modifications (All Rounds)
# ========================================
# Plan backup is read-only - protects plan integrity during loop
# Use command_modifies_file helper for consistent pattern matching

if command_modifies_file "$COMMAND_LOWER" "\.humanize/rlcr(/[^/]+)?/plan\.md"; then
    FALLBACK="Writing to plan.md backup is not allowed during RLCR loop."
    REASON=$(load_and_render_safe "$TEMPLATE_DIR" "block/plan-backup-protected.md" "$FALLBACK")
    echo "$REASON" >&2
    exit 2
fi

# ========================================
# Block Goal Tracker Modifications (All Rounds)
# ========================================
# Round 0: prompt to use Write/Edit
# Round > 0: prompt to put request in summary

if command_modifies_file "$COMMAND_LOWER" "goal-tracker\.md"; then
    GOAL_TRACKER_PATH="$ACTIVE_LOOP_DIR/goal-tracker.md"
    if [[ "$CURRENT_ROUND" -eq 0 ]]; then
        goal_tracker_bash_blocked_message "$GOAL_TRACKER_PATH" >&2
    else
        goal_tracker_blocked_message "$CURRENT_ROUND" "$GOAL_TRACKER_PATH" >&2
    fi
    exit 2
fi

# ========================================
# Block Prompt File Modifications (All Rounds)
# ========================================
# Prompt files are read-only - they contain instructions FROM Codex TO Claude

if command_modifies_file "$COMMAND_LOWER" "round-[0-9]+-prompt\.md"; then
    prompt_write_blocked_message >&2
    exit 2
fi

# ========================================
# Block Summary File Modifications (All Rounds)
# ========================================
# Summary files should be written using Write or Edit tools for proper validation

if command_modifies_file "$COMMAND_LOWER" "round-[0-9]+-summary\.md"; then
    CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-summary.md"
    summary_bash_blocked_message "$CORRECT_PATH" >&2
    exit 2
fi

# ========================================
# Block Round Contract File Modifications (All Rounds)
# ========================================
# Round contracts should be written using Write or Edit tools so round scoping
# stays aligned with the current loop state.

if command_modifies_file "$COMMAND_LOWER" "round-[0-9]+-contract\.md"; then
    CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-contract.md"
    FALLBACK="# Round Contract Bash Write Blocked

Do not use Bash commands to modify round contract files.
Use the Write or Edit tool instead: {{CORRECT_PATH}}"
    load_and_render_safe "$TEMPLATE_DIR" "block/round-contract-bash-write.md" "$FALLBACK" \
        "CORRECT_PATH=$CORRECT_PATH" >&2
    exit 2
fi

# ========================================
# Block Todos File Modifications (All Rounds)
# ========================================

if command_modifies_file "$COMMAND_LOWER" "round-[0-9]+-todos\.md"; then
    # Require full path to active loop dir to prevent same-basename bypass from different roots
    ACTIVE_LOOP_DIR_LOWER=$(to_lower "$ACTIVE_LOOP_DIR")
    ACTIVE_LOOP_DIR_ESCAPED=$(echo "$ACTIVE_LOOP_DIR_LOWER" | sed 's/[\\.*^$[(){}+?|]/\\&/g')
    if ! echo "$COMMAND_LOWER" | grep -qE "${ACTIVE_LOOP_DIR_ESCAPED}/round-[12]-todos\.md"; then
        todos_blocked_message "Bash" >&2
        exit 2
    fi
fi

fi  # End of RLCR-specific checks

exit 0
