#!/usr/bin/env bash
# _git-guard-shim.sh — PATH shim for `git`. Blocks the SAME destructive verbs as
# main-checkout-guard.sh (git restore / checkout -- / checkout . / stash push,pop,
# drop,clear / clean -f* / reset --hard) when they target the LIVE deploy clone
# (OVERDECK_DEPLOY_DIR), or the SHARED main checkout of a repo that has opted into
# worktree isolation (a <main_root>/.worktrees dir).
#
# WHY A PATH SHIM, NOT JUST A CLAUDE HOOK: Claude Code's PreToolUse hook
# (main-checkout-guard.sh) covers only Claude's own Bash tool. codex (`cdx exec`,
# used by /ask-codex) and cursor-agent run their own internal git calls with no
# equivalent hook. PATH is inherited across every exec boundary — the same
# rationale _tmpjail-shim.sh documents for wrapping claude/codex/cursor-agent
# themselves — so wrapping `git` here catches all three runtimes, and anything
# else spawned underneath them, without depending on per-runtime cooperation.
#
# WHY GATED ON AGENT_BUILD_SCOPE_ACTIVE: that var is exported unconditionally by
# _agent-build-scope for every launch of the wrapped claude/codex/cursor-agent
# CLIs, and inherited by every process they spawn. A human's own interactive
# shell never sets it, so a person typing `git restore` by hand is never
# affected — only git commands running inside an agent-launched process tree are.
#
# GIT_GUARD_SHIM_MARKER OD_PATH_SHIM_MARKER — identify any copy of this shim to
# install-git-guard-real and to the sibling shims' PATH resolvers.
#
# The real git is PINNED at install time (install-git-guard-real), never searched
# for at run time: a shim that looks nothing up cannot resolve into a copy of
# itself. Every unresolvable state below exits non-zero; none re-invokes `git`.
set -e

name="${0##*/}"
SHIM_DIR="$(cd "$(dirname "$(readlink -f -- "${BASH_SOURCE[0]}")")" && pwd)"

GUARD_LIB="$SHIM_DIR/../lib/shim-guard.sh"
if [[ ! -r "$GUARD_LIB" ]]; then
  echo "$name: git-guard shim cannot read $GUARD_LIB — refusing" >&2
  exit 78
fi
# shellcheck source=../lib/shim-guard.sh
source "$GUARD_LIB"
shim_guard_enter "$name"

if ! shim_pin_read git; then
  echo "$name: git-guard shim has no usable pinned git (looked in: $SHIM_PIN_TRIED) — run $SHIM_DIR/install-git-guard-real" >&2
  exit 78
fi
real="$SHIM_PIN_REAL"
shim_guard_clear "$name"

if [[ "$SHIM_REENTRY" == 1 ]]; then
  exec "$real" "$@"
fi

# SUGGEST band: advisory only, never blocks. Gated to command shapes a suggest rule
# could plausibly match (cheap literal check first, same rationale as the destructive-
# verb case below) and to agent-launched processes only — a human's interactive git
# never pays this. This is the codex/cursor-agent-covering half of the shared matcher
# deny-gate.mjs also uses (see hooks/lib/tool-rules.mjs) — same tools.json, same rules.
if [[ "${AGENT_BUILD_SCOPE_ACTIVE:-}" == "1" ]]; then
  case "$*" in
    *worktree*) node "$SHIM_DIR/../hooks/lib/tool-suggest-check.mjs" "git $*" >&2 & disown ;;
  esac
fi

# MASS-DELETE commit guard: a partially-materialized worktree plus `git add -A` stages
# every absent file as a deletion, and filtered tool output can hide all of it — a blind
# agent commit then records thousands of deletions (2026-08-15: 3,840, caught pre-land).
# Refuse an agent commit staging an implausible number of deletions; a deliberate mass
# deletion states its intent via GIT_ALLOW_MASS_DELETE=1. Humans never pay this (scope
# gate), and any failure of the check itself falls open to the real git.
if [[ "${AGENT_BUILD_SCOPE_ACTIVE:-}" == "1" && "${GIT_ALLOW_MASS_DELETE:-}" != "1" ]]; then
  case "$*" in
    *commit*)
      _del_count=$("$real" diff --cached --name-status 2>/dev/null | grep -c '^D') || _del_count=0
      if [[ "$_del_count" =~ ^[0-9]+$ ]] && (( _del_count > 100 )); then
        cat >&2 <<MSG
git-guard: this commit stages $_del_count file DELETIONS. That usually means the working
tree is partially materialized (interrupted checkout) and add -A recorded every missing
file as deleted — committing would destroy them in history. Verify the tree first:
  $real status --porcelain | head -50        # look for walls of ' D ' lines
  $real ls-tree -r HEAD --name-only | wc -l  # compare against the base branch
A deliberate mass deletion re-runs with GIT_ALLOW_MASS_DELETE=1.
MSG
        exit 77
      fi
      ;;
  esac
fi

# Every pattern wt_has_destructive_git matches requires one of these literals in argv.
# With none present the guard cannot fire, so the path resolution below — 3 realpath and
# 2 perl starts on every git an agent runs — is skipped.
case "$*" in
  *restore*|*checkout*|*stash*|*clean*|*reset*) ;;
  *) exec "$real" "$@" ;;
esac

if [[ "${AGENT_BUILD_SCOPE_ACTIVE:-}" == "1" ]]; then
  LIB="$SHIM_DIR/../hooks/lib/worktree-guard-lib.sh"
  if [[ -r "$LIB" ]]; then
    # shellcheck source=../hooks/lib/worktree-guard-lib.sh
    source "$LIB"
    export WT_GIT_BIN="$real"

    # Every path compared below goes through wt_resolve: ~/.claude/bin, ~/.claude/hooks
    # and ~/.claude/lib are symlinks INTO the deploy clone, and an unresolved compare
    # lets a redirect through one of them miss both roots entirely.
    EFFECTIVE_DIR="$(wt_resolve "$PWD")"
    while IFS= read -r target; do
      [[ -z "$target" ]] && continue
      [[ "$target" != /* ]] && target="$EFFECTIVE_DIR/$target"
      EFFECTIVE_DIR="$(wt_resolve "$target")"
    done < <(wt_git_dir_targets "$*")

    DEPLOY_ROOT="$(wt_deploy_root)"

    if wt_has_destructive_git "git $*"; then
      if wt_is_within "$EFFECTIVE_DIR" "$DEPLOY_ROOT"; then
        cat >&2 <<MSG
git-guard: this command discards/overwrites working-tree state in the LIVE deploy clone
($DEPLOY_ROOT), which every agent's hooks, skills and landers execute from, and which
the next deploy rebuilds from main. Change the repo and land it instead:
  ~/.claude/bin/od-worktree add <slug>
  edit under the worktree, then ship.sh land wt/<slug> <worktree>
MSG
        exit 77
      fi

      if MAIN_ROOT=$(wt_main_root "$EFFECTIVE_DIR") \
        && [[ -d "$MAIN_ROOT/.worktrees" ]] \
        && wt_is_main_checkout_path "$EFFECTIVE_DIR" "$MAIN_ROOT"; then
        cat >&2 <<MSG
git-guard: this command discards/overwrites working-tree state in the SHARED main
checkout ($MAIN_ROOT) — it can silently destroy another agent session's uncommitted
edits. Work in a claimed worktree instead:
  ~/.claude/bin/od-worktree add <slug>
  cd $MAIN_ROOT/.worktrees/<slug>
MSG
        exit 77
      fi
    fi
  fi
fi

exec "$real" "$@"
