#!/bin/bash
# git-mutation-guard — PreToolUse(Bash) guard against destructive git operations
# that discard uncommitted work in a worktree shared with background agents.
#
# Contract: reads the tool-call JSON on stdin, extracts .tool_input.command.
#   exit 0 = allow, exit 2 = block (non-zero exit blocks the Bash command).
# Fails OPEN (exit 0) on any parse ambiguity so it never wedges legitimate work.
#
# OUTPUT ROUTING (KI-HOOKS-STDERR-SYSTEMIC): the harness surfaces a blocking
# hook's STDERR, not its stdout. All refuse() output goes to stderr (>&2) so the
# agent actually sees WHY a command was blocked. The allow paths stay silent.
#
# WHY (KI-SHARED-WORKTREE-GIT-MUTATION): impl agents and the leader share one
# worktree. `git stash`, `git reset --hard`, `git checkout -- <path>`, and
# `git restore` (worktree) silently delete another agent's uncommitted edits —
# three real work-loss incidents on 2026-07-03. Commit at gate points; never
# discard. This guard gates v3.84 Step 0.
#
# ESCAPE HATCH: prefix the command with `GIT_EXPERT=1` to run a genuinely-needed
# destructive git op deliberately (e.g. GIT_EXPERT=1 git reset --hard <sha>).
# The escape is intentional friction, not a lock.
# OVERRIDE AUDIT (2026-07-31 false alarm): an override that clears a real block is
# appended to ~/.mx-agent/logs/git-guard-override.jsonl before the command runs,
# so "the guard never fired" and "the guard fired and was overridden" are
# distinguishable after the fact. Motivating incident: a git-expert was blocked on
# a hard reset, re-ran it four seconds later with GIT_EXPERT=1, and reported only
# the reset to its leader; that leader then reported a broken guard in good faith
# and an investigation confirmed the guard was healthy in all eight worktrees. The
# override itself was sanctioned — its invisibility was the defect. Self-reporting
# alone is a rung-5 control (it depends on the overriding agent choosing to
# mention it), so the log exists as the independent record. Logging is FAIL-OPEN
# and side-effect-only: on any problem it is skipped, not retried and not
# escalated, so an override is neither withheld nor delayed because of it. See
# the log_override() header for the failure modes this had to be hardened
# against (a FIFO that blocked the append forever, a symlink that redirected it)
# and for the residual TOCTOU that is knowingly accepted.
# LAYERING CAVEAT: GIT_EXPERT=1 only clears THIS hook's blocks (bare-SHA/branch
# reset forms, stash, checkout-file, restore-worktree). It does NOT clear the two
# patterns `git reset --hard HEAD~*` and `git reset --hard HEAD^*` — those are
# hard-denied at the Claude Code permission layer (.claude/settings.json deny
# list), which fires BEFORE this hook runs. If an override appears to "fail" on a
# HEAD~/HEAD^ reset, debug the settings.json deny list, not this script.
#
# This guard ONLY inspects git mutation patterns; all non-git and read-only git
# commands (status, log, diff, stash list, stash show, restore --staged) pass.
#
# SCOPE (SF-2, QA probe audit 2026-07-06 — this is a guardrail, not a security
# boundary). INERT-CONSUMER HEREDOC BODIES ARE EXCLUDED FROM MATCHING: a git verb
# inside a heredoc whose consuming command does NOT execute the body (e.g. a
# status report written via `cat <<EOF ... git reset --hard ... EOF`, or a body
# fed to tee / a redirect-to-file / a non-shell interpreter) is data, not a
# command, so it is stripped before matching. This removes the reported false-
# positive class that blocked report/heredoc writes (RT-3 / block-report
# 2026-07-06). GUARANTEE (corrected after security re-review): the strip is
# consumer-aware — a heredoc whose operator line names a SHELL interpreter
# (bash/sh/zsh/dash/ksh/eval/source/`.`) or pipes the body into one (`| ...sh`)
# is KEPT and still scanned, because those bodies ARE executed and a destructive
# git verb in them is real (e.g. `bash <<EOF ... git reset --hard ... EOF`).
# QUOTED-CONTENT HANDLING (per-segment sink scoping, #3938): commands whose
# quoted text merely MENTIONS destructive git verbs (e.g. `echo "run git reset
# --hard"`, `gh issue create --body "... git stash ..."`) are correctly allowed.
# Sink commands (echo, printf, gh --body, git commit -m, etc.) have their quoted
# content blanked before scanning. Executor commands (bash, ssh, su, etc.) have
# their quoted content extracted and scanned as separate segments — their args
# are commands, not data. Pipe chains propagate executor status backwards, so
# `echo "git reset --hard" | bash` is still blocked. Unknown commands keep
# quotes intact (fail safe = blocked if a pattern matches). Known bypass classes
# (out of scope, NOT defended): indirection via a script file, shell
# variable/`eval` assembly, or an alias can run a blocked op without a literal
# match. The guard deters accidental discards; it does not stop a determined
# caller.

set -euo pipefail

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // .command // empty' 2>/dev/null || true)

# Can't parse — fail open.
if [[ -z "$COMMAND" ]]; then
  exit 0
fi

# Deliberate override. NOTE: this no longer short-circuits. The scan still runs so
# we can name WHICH pattern the override cleared; refuse() then logs and allows
# instead of blocking. A GIT_EXPERT=1 command that matches nothing is not an
# override of anything and is not logged.
#
# LOG FIDELITY (#4804, part 1 of 2). OVERRIDE below is a WHOLE-COMMAND test and
# still governs the allow/deny decision exactly as before — this commit changes
# no boundary. What it changes is what gets WRITTEN: a record is appended only
# when the override token and the pattern that matched occur in the SAME command
# segment (SEG_OVERRIDE, set in check_segment). The whole-command test made the
# log useless as evidence — a read-only command that merely mentions both the
# token and a destructive verb somewhere in its text (an agent writing about the
# guard, a probe script being edited) produced a record indistinguishable from a
# genuine override. All four records in the live log were produced that way; none
# was a real override. Same-segment is the weakest condition that separates "the
# token was applied to this command" from "the token appears somewhere on the
# line", and it is decision-neutral, so it needs no boundary re-verification.
OVERRIDE_RE='(^|[^A-Za-z_])GIT_EXPERT=1(\s|$)'
OVERRIDE=0
if echo "$COMMAND" | grep -qE "$OVERRIDE_RE"; then
  OVERRIDE=1
fi
# Set per segment by check_segment; consumed by refuse() for logging only.
SEG_OVERRIDE=0

# --- override audit log (fail-open, side-effect only) -------------------------
# One JSONL line per cleared block, alongside idle-gate.jsonl / session-pause.jsonl.
# Fields: ts, worktree, guard, pattern (what was blocked), command (first line,
# sanitized + truncated). The command is UNTRUSTED text, so it is reduced to a
# conservative allowlist — no quotes, backslashes, control characters or non-ASCII
# reach the file, and nothing that renders as markup survives.
#
# PATH IS HARD-CODED, deliberately. There is no env var to redirect or disable
# this log: the party whose action is being recorded is the party who benefits
# from suppressing it, so a single variable in the environment must not be able
# to turn the record off. Same shape as session-idle-gate.sh, which hard-codes
# its own path. The probe isolates itself by overriding HOME, not by asking the
# product for a test hook.
#
# WRITE TARGET CHECKS (security review, 2026-08-01). Three defects were measured
# against earlier revisions of this file, each reproduced before it was fixed:
#   - a FIFO at the log path with no reader made the append block forever on
#     open(); `timeout 8` returned 124. Every override in that home directory
#     hung the Bash call with no error output. `set +e` catches failures; it does
#     nothing about blocking, so the claim that no logging failure could convert
#     an allowed command into a blocked one was false — the command never ran.
#   - a symlink at the log FILE path was followed, appending the record to the
#     link target (201 bytes into a file outside the log directory).
#   - after the leaf check landed, the same redirect survived one level up:
#     `mkdir -p` and `>>` both follow DIRECTORY symlinks, so a symlinked
#     `~/.mx-agent/logs` still took 185 bytes into an attacker-chosen directory.
# So there are two tests: the leaf must be absent or a plain regular file and not
# a symlink (`-f` alone follows links), and the containing directory must be a
# real directory and not a symlink. Either test failing skips the log entirely
# and lets the override proceed.
#
# WHAT THIS DOES NOT CLOSE — read this before describing the log as tamper-proof,
# because that description has now been wrong three times. Symlink redirection is
# closed AT THE LEAF AND ONE DIRECTORY UP. It is NOT closed for a symlinked
# `~/.mx-agent`, a symlinked `$HOME`, or any higher path component: those are
# resolved by the kernel before either test sees them, and walking every
# component is not something this hook should be doing on a hot path. Also not
# closed: the checks are TOCTOU, not atomic — a writer that swaps the path
# between the test and the append still wins the race, and closing that needs
# O_NOFOLLOW|O_NONBLOCK, which bash cannot express. Both residuals are accepted
# on the same grounds: anyone able to plant a component symlink in the agent's
# own home or to win that race already executes as the agent user, and this file
# is a guardrail, not an access control (same posture as the guard itself).
#
# The whole body runs in a subshell with errexit disabled, so an error inside it
# cannot change the caller's exit status. Combined with the two target tests, the
# intended property is that logging is skipped rather than allowed to stall or
# misdirect — an override is never withheld, and never delayed, because of the
# log.
log_override() {
  (
    set +e +u +o pipefail
    log_dir="${HOME:-/tmp}/.mx-agent/logs"
    log_file="${log_dir}/git-guard-override.jsonl"

    # Leaf: refuse anything that is not an absent path or a plain regular file.
    # FIFOs block, symlinks redirect, devices do neither usefully.
    if [ -e "$log_file" ] || [ -L "$log_file" ]; then
      [ -f "$log_file" ] && [ ! -L "$log_file" ] || exit 0
    fi

    ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    wt_path="${CLAUDE_WORKTREE_PATH:-${CLAUDE_PROJECT_DIR:-}}"
    wt="$(basename "$wt_path" 2>/dev/null)"
    wt="${wt//[^a-zA-Z0-9_.-]/}"
    wt="${wt:-unknown}"
    cmd="$(printf '%s' "$COMMAND" \
      | head -n 1 \
      | LC_ALL=C tr -d '\000-\037\177' \
      | LC_ALL=C tr -c 'A-Za-z0-9 _.,:/=@+~^-' '?' \
      | cut -c1-200)"
    pat="$(printf '%s' "$1" | LC_ALL=C tr -c 'A-Za-z0-9 _.<>-' '?' | cut -c1-60)"
    mkdir -p "$log_dir" || exit 0

    # Containing directory: `mkdir -p` succeeds on a symlink-to-directory and
    # `>>` follows it, so the leaf check alone left the redirect intact one level
    # up. Test after mkdir, since the directory may not have existed before it.
    [ -d "$log_dir" ] && [ ! -L "$log_dir" ] || exit 0

    printf '{"ts":"%s","worktree":"%s","guard":"git-mutation-guard","pattern":"%s","override":"GIT_EXPERT=1","command":"%s"}\n' \
      "$ts" "$wt" "$pat" "$cmd" >> "$log_file"
  ) >/dev/null 2>&1 || true
  return 0
}

# --- heredoc-body stripping (consumer-aware) --------------------------------
# Produce a "scannable" copy of the command. A heredoc body is dropped ONLY when
# its consuming command is inert (cat/tee/redirect-to-file/non-shell language) —
# such a body is data, never executed. When the operator line names a SHELL
# interpreter (bash/sh/zsh/dash/ksh/eval/source/`.`) or pipes the body into one
# (`| ...sh`), the body IS executed, so it is KEPT and still scanned (a
# destructive git verb in it is a real command, not prose). The operator line
# itself is always preserved (it may hold a real git verb before the `<<`).
# Covers the common forms: `<<EOF`, `<<-EOF`, `<<"EOF"`, `<<'EOF'`, `<< EOF`.
strip_heredocs() {
  awk '
    !inbody {
      line = $0
      if (match(line, /<<[-]?[[:space:]]*["'"'"']?[A-Za-z_][A-Za-z0-9_]*["'"'"']?/)) {
        d = substr(line, RSTART, RLENGTH)
        gsub(/<<[-]?[[:space:]]*["'"'"']?/, "", d)
        gsub(/["'"'"'].*/, "", d)
        delim = d
        inbody = 1
        # keepbody: does a SHELL execute this body? If so, still scan it.
        keepbody = 0
        if (line ~ /(^|[^[:alnum:]_])(bash|sh|zsh|dash|ksh|eval|source)([^[:alnum:]_]|$)/) keepbody = 1
        if (line ~ /(^|[[:space:]])\.[[:space:]]/) keepbody = 1
        if (line ~ /\|[^|]*(bash|sh|zsh|dash|ksh|eval)([^[:alnum:]_]|$)/) keepbody = 1
      }
      print line
      next
    }
    {
      t = $0
      gsub(/^[[:space:]]+/, "", t)
      if (t == delim) { inbody = 0; keepbody = 0; next }
      if (keepbody) print $0
      # else: inert-consumer body — data, not a command; drop it from the scan
    }
  '
}

# --- per-segment sink-scoped quote handling (#3938) ---------------------------
# Sink commands (echo, printf, gh --body, git commit -m, etc.) have their
# quoted content blanked so prose mentioning destructive git verbs doesn't
# false-positive. Executor commands (bash, ssh, su, etc.) have their quoted
# content EXTRACTED and emitted as additional segments for scanning (their
# quoted args are commands, not data). Per-segment scoping prevents the
# round-2 bypass where a sink on one segment blanked an executor's payload
# on the next. Pipe chains propagate executor status backwards: if stage N
# is an executor and connected by pipe to stage N-1, stage N-1's quotes
# are not blanked (its output feeds the executor). Fail-safe: unknown
# commands keep quotes intact (no blanking, no extraction).
strip_sink_quotes() {
  awk '
function get_lead_cmd(seg,    s) {
    s = seg; gsub(/^[[:space:]]+/, "", s)
    while (match(s, /^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+/))
        s = substr(s, RSTART + RLENGTH)
    while (match(s, /^[0-9]*[<>][>&]?[^[:space:]]*[[:space:]]+/))
        s = substr(s, RSTART + RLENGTH)
    if (match(s, /^[^[:space:]]+/)) return substr(s, RSTART, RLENGTH)
    return ""
}
function is_exec(cmd) {
    return (cmd == "bash" || cmd == "sh" || cmd == "zsh" || cmd == "dash" || \
            cmd == "ksh" || cmd == "eval" || cmd == "source" || cmd == "ssh" || \
            cmd == "su" || cmd == "doas" || cmd == "nsenter" || \
            cmd == "sudo" || cmd == "xargs")
}
function check_sink(seg,    cmd, s, sc) {
    cmd = get_lead_cmd(seg)
    if (is_exec(cmd)) return 0
    if (cmd == "echo" || cmd == "printf" || cmd == "cat" || cmd == "tee" || cmd == "write")
        return 1
    if (cmd == "gh" && (seg ~ /--body/ || seg ~ /--title/ || seg ~ /--comment/))
        return 1
    if (cmd == "git") {
        s = seg; gsub(/^[[:space:]]+/, "", s); sub(/^git[[:space:]]+/, "", s)
        while (match(s, /^(-C[[:space:]]+[^[:space:]]+|-c[[:space:]]+[^[:space:]]+|--[a-z][-a-z0-9]*(=[^[:space:]]*)?)[[:space:]]+/))
            s = substr(s, RSTART + RLENGTH)
        if (match(s, /^[^[:space:]]+/)) {
            sc = substr(s, RSTART, RLENGTH)
            if ((sc == "commit" || sc == "tag" || sc == "notes") && \
                (seg ~ /-[a-zA-Z]*m[[:space:]]/ || seg ~ /--message/))
                return 1
        }
    }
    return 0
}
function blank_quotes(seg,    n, i, c, out, isq, idq) {
    n = length(seg); out = ""; isq = 0; idq = 0
    for (i = 1; i <= n; i++) {
        c = substr(seg, i, 1)
        if (isq) {
            if (c == "'"'"'") { isq = 0; out = out c } else out = out " "
            continue
        }
        if (idq) {
            if (c == "\\") { out = out "  "; i++; continue }
            if (c == "\"") { idq = 0; out = out c } else out = out " "
            continue
        }
        if (c == "'"'"'") { isq = 1; out = out c; continue }
        if (c == "\"") { idq = 1; out = out c; continue }
        out = out c
    }
    return out
}
function extract_quotes(seg,    n, i, c, ct, isq, idq, res) {
    n = length(seg); isq = 0; idq = 0; ct = ""; res = ""
    for (i = 1; i <= n; i++) {
        c = substr(seg, i, 1)
        if (isq) {
            if (c == "'"'"'") {
                isq = 0
                if (ct != "") { if (res != "") res = res "\n"; res = res ct }
                ct = ""
            } else ct = ct c
            continue
        }
        if (idq) {
            if (c == "\\") { ct = ct substr(seg, i+1, 1); i++; continue }
            if (c == "\"") {
                idq = 0
                if (ct != "") { if (res != "") res = res "\n"; res = res ct }
                ct = ""
            } else ct = ct c
            continue
        }
        if (c == "'"'"'") { isq = 1; continue }
        if (c == "\"") { idq = 1; continue }
    }
    return res
}
{
    line = $0; n = length(line)
    isq = 0; idq = 0; num = 0; seg_s = 1
    for (i = 1; i <= n; i++) {
        c = substr(line, i, 1)
        if (isq) { if (c == "'"'"'") isq = 0; continue }
        if (idq) { if (c == "\\") { i++; continue }; if (c == "\"") idq = 0; continue }
        if (c == "'"'"'") { isq = 1; continue }
        if (c == "\"") { idq = 1; continue }
        sp = 0; sl = 1; st = ""
        if (c == ";") { sp = 1; st = "seq" }
        else if (c == "&" && i < n && substr(line, i+1, 1) == "&") { sp = 1; sl = 2; st = "seq" }
        else if (c == "|" && i < n && substr(line, i+1, 1) == "|") { sp = 1; sl = 2; st = "seq" }
        else if (c == "|") { sp = 1; st = "pipe" }
        if (sp) {
            num++; segs[num] = substr(line, seg_s, i - seg_s); stypes[num] = st
            seg_s = i + sl; if (sl == 2) i++
        }
    }
    num++; segs[num] = substr(line, seg_s); stypes[num] = ""
    # classify each segment
    for (s = 1; s <= num; s++) { snk[s] = check_sink(segs[s]); pxe[s] = 0 }
    # backward-propagate executor status through pipes
    for (s = num; s >= 2; s--) {
        if (stypes[s-1] == "pipe") {
            cmd = get_lead_cmd(segs[s])
            if (is_exec(cmd) || pxe[s]) { snk[s-1] = 0; pxe[s-1] = 1 }
        }
    }
    # emit processed segments
    for (s = 1; s <= num; s++) {
        if (snk[s]) { print blank_quotes(segs[s]) }
        else {
            print segs[s]
            cmd = get_lead_cmd(segs[s])
            if (is_exec(cmd) || pxe[s]) {
                ex = extract_quotes(segs[s])
                if (ex != "") print ex
            }
        }
    }
}
  '
}

SCAN=$(printf '%s' "$COMMAND" | strip_heredocs)

refuse() {
  local what="$1"

  # Override in effect: record what it cleared, then allow. Silent on stdout and
  # stderr — the allow contract is "exit 0, no output".
  #
  # The ALLOW is decided by the whole-command OVERRIDE (unchanged). The RECORD is
  # written only when the token sits in the same segment as the pattern that
  # matched (#4804): a command whose token and pattern are in different segments
  # is still allowed exactly as before, but it is not evidence of an override and
  # must not be written down as one.
  if [[ "$OVERRIDE" -eq 1 ]]; then
    if [[ "$SEG_OVERRIDE" -eq 1 ]]; then
      log_override "$what"
    fi
    exit 0
  fi

  {
    echo ""
    echo "BLOCKED: destructive git operation ($what)"
    echo "  Command: $COMMAND"
    echo ""
    echo "This discards uncommitted work in a worktree shared with background"
    echo "agents (KI-SHARED-WORKTREE-GIT-MUTATION — three work-loss incidents"
    echo "2026-07-03). Commit at gate points instead of discarding."
    echo ""
    echo "If you truly must run this, prefix it with GIT_EXPERT=1 to override,"
    echo "e.g.  GIT_EXPERT=1 $what ..."
    echo ""
    echo "IF YOU OVERRIDE, YOU MUST REPORT IT. Your report back to whoever asked"
    echo "for this work has to state: the command that was blocked, that"
    echo "git-mutation-guard blocked it, that you used GIT_EXPERT=1 to override,"
    echo "and why the override was justified. Omitting this is a failure to"
    echo "complete the task — a silent override on 2026-07-31 was reported up the"
    echo "chain as a broken guard and cost an investigation across eight"
    echo "worktrees. The override is recorded either way."
    echo ""
  } >&2
  exit 2
}

# --- command-segment anchoring (M3, KI-HOOKS-PROSE-FALSE-POSITIVE) ----------
# A `git` verb only counts as a real invocation when it STARTS a command
# segment. Matching the bare substring `git reset --hard` anywhere in the
# command blocked legitimate commit messages and prose, e.g.
#   git commit -m "reverted the git reset --hard"
#   echo "run git reset --hard to undo"
# GITSEG requires `git` at a segment boundary — start-of-line (grep is
# line-oriented, so embedded newlines are segment starts too), after a
# separator (`;` `&` `|` `(` `{`), or after a shell keyword (then/do/else) —
# optionally preceded by any run of: leading redirections (`>out `,
# `2>/dev/null `), env-var assignments (`VAR=1 `), and known launcher/wrapper
# words that exec their argv (sudo/env/nice/nohup/time/timeout/ionice/stdbuf/
# command/xargs/setsid, with an optional numeric arg like `timeout 5`). Without
# the wrapper/redirection tolerance, `nice git reset --hard`,
# `2>/dev/null git reset --hard`, etc. slipped past the anchor (security
# re-review, #3912).
#
# GIT GLOBAL OPTIONS between `git` and the verb are consumed (High, #3934). git
# accepts global options before the subcommand — `-C <path>`, `-c <key>=<val>`,
# `--git-dir[=| ]<p>`, `--work-tree[=| ]<p>`, `--namespace[=| ]<p>`,
# `--no-pager`, and other single/double-dash forms with or without values. Only
# consuming `-C` (the pre-#3934 behavior) let `git --no-pager reset --hard`,
# `git -c safe.directory=* reset --hard`, `git --git-dir=... reset --hard`
# bypass the guard entirely. The repeated GOPT group below consumes the whole
# global-option run so the blocked verb is still anchored. `-C`/`-c`/the
# enumerated value-taking long options consume their following value; a generic
# `--long`/`--long=val`/`-x` handles the no-value forms (like --no-pager). This
# mirrors the PUSH_RE global-option handling in ci-validate-hook.sh /
# mx-guard-hook.sh. Applied per-segment to the heredoc- and quote-stripped SCAN.
# GIT_EXPERT=1 clears any accepted false positive. `echo` is deliberately NOT a
# wrapper word, so `echo "use timeout 5 git reset --hard"` stays allowed.
GITSEG='(^|[;&|({`]|\s(then|do|else)\s)\s*([0-9]*[<>][^[:space:]]*\s+|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*\s+|(sudo|env|nice|nohup|time|timeout|ionice|stdbuf|command|xargs|setsid|doas|nsenter)\s+([0-9]+\s+)?)*git(\s+(-C\s+[^[:space:];&|]+|-c\s+[^[:space:];&|]+|--(git-dir|work-tree|namespace|super-prefix)(=[^[:space:];&|]*|\s+[^[:space:];&|]+)|--[A-Za-z][A-Za-z0-9-]*=[^[:space:];&|]*|--[A-Za-z][A-Za-z0-9-]*|-[A-Za-z]))*\s+'

# --- per-segment evaluation (item 3, #3934; sink scoping, #3938) --------------
# Each allow-check (stash list|show; restore --staged-without-worktree) must be
# scoped to the SAME command segment as the mutating verb it whitelists.
# Whole-command allow-checks let a read-only first segment green-light a
# destructive second one: `git stash list && git stash drop` or
# `git restore --staged f && git restore f` slipped through because the
# allow-form matched elsewhere on the line. strip_sink_quotes splits SCAN on
# unquoted shell separators (`;` `&&` `||` `|`), blanks quoted content in sink
# segments, extracts quoted content from executor segments as additional lines,
# and emits one segment per line. refuse() exits, so the first offending
# segment blocks.
check_segment() {
  local seg="$1"

  # Does the override token appear in THIS segment (#4804)? Logging-only in this
  # revision: refuse() still decides allow/deny from the whole-command OVERRIDE.
  SEG_OVERRIDE=0
  if printf '%s' "$seg" | grep -qE "$OVERRIDE_RE"; then
    SEG_OVERRIDE=1
  fi

  # git stash (mutating forms). Allow read-only `stash list` / `stash show`;
  # block bare stash, push, pop, apply, drop, clear, save, branch, create, store.
  if printf '%s' "$seg" | grep -qE "${GITSEG}stash\b"; then
    if ! printf '%s' "$seg" | grep -qE "${GITSEG}stash\s+(list|show)\b"; then
      refuse "git stash"
    fi
  fi

  # git reset --hard
  if printf '%s' "$seg" | grep -qE "${GITSEG}reset\s+([^&|;]*\s)?--hard\b"; then
    refuse "git reset --hard"
  fi

  # git checkout <ref?> -- <path> (pathspec form discards working changes).
  # The trailing `(\s|$)` after `--` means `--help`/`--force`/branch switches do
  # NOT match (only the bare `--` pathspec separator does).
  if printf '%s' "$seg" | grep -qE "${GITSEG}checkout\s+([^&|;]*\s)?--(\s|$)"; then
    refuse "git checkout -- <path>"
  fi

  # git restore (worktree). `git restore` defaults to --worktree (destructive).
  # Safe ONLY when --staged is present AND --worktree is absent (a pure unstage).
  # Block if --worktree is explicit, or if --staged is missing.
  if printf '%s' "$seg" | grep -qE "${GITSEG}restore\b"; then
    if printf '%s' "$seg" | grep -qE "${GITSEG}restore\b[^&|;]*--worktree\b"; then
      refuse "git restore --worktree"
    elif ! printf '%s' "$seg" | grep -qE "${GITSEG}restore\b[^&|;]*--staged\b"; then
      refuse "git restore <path> (defaults to --worktree)"
    fi
  fi
}

while IFS= read -r _seg; do
  [[ -n "$_seg" ]] || continue
  check_segment "$_seg"
done < <(printf '%s\n' "$SCAN" | strip_sink_quotes)

exit 0
