#!/usr/bin/env bash
# Auto-generated by nerftools: PreToolUse multi-check dispatcher.
#
# Runs the following opt-in checks against the Bash command the agent is
# about to run. Checks run in declaration order; the first denial short-
# circuits the rest. Each check is independently env-var gated and
# brand-namespaced so multiple plugins with different brands can coexist.
#
# 1. Current-version check  (env: NERF_ENABLE_CURRENT_VERSION_HOOK)
#    Refuses to run a tool invocation whose path points at an OLD or NEW
#    version of THIS plugin (catches stale tool paths that an agent may
#    have cached from a prior session after a plugin upgrade). Matches
#    on plugin owner, plugin name (including brand), and tool-name brand
#    prefix to avoid false positives on other plugins. NO bypass
#    sentinel -- this check is intentionally strict.
#
# 2. Bash-hint check        (env: NERF_ENABLE_BASH_HINT_HOOK)
#    Refuses raw bash commands matching a wrapper's bash_hints pattern;
#    redirects the agent to the wrapper. Bypassable via the sentinel
#    `# nerf:bypass-bash-hint <reason>` (non-empty reason required).
#
# Fail-open: if bash < 4 or jq is missing, this hook exits 0 silently.
# A SessionStart hook surfaces a warning so the user knows the redirect
# is disabled.

if [[ "${BASH_VERSINFO[0]:-0}" -lt 4 ]]; then
  exit 0
fi

set -uo pipefail

_WRAPPER_PREFIX="nerf-"
_BRAND="nerf"
_BRAND_RE="nerf"
_PLUGIN_NAME="nerftools"

# Tab-separated <regex>\t<skill> rows, declaration order. \b boundaries in
# manifest patterns are translated to portable POSIX ERE at generation time
# so the hook works on macOS BSD regex (where \b is not supported).
_PATTERNS=(
  $'(^|$|[^[:alnum:]_])az account(^|$|[^[:alnum:]_])	nerf-az-account'
  $'(^|$|[^[:alnum:]_])az aks(^|$|[^[:alnum:]_])	nerf-az-aks'
  $'(^|$|[^[:alnum:]_])az boards(^|$|[^[:alnum:]_])	nerf-az-boards'
  $'(^|$|[^[:alnum:]_])az cosmosdb(^|$|[^[:alnum:]_])	nerf-az-cosmosdb'
  $'(^|$|[^[:alnum:]_])az devops(^|$|[^[:alnum:]_])	nerf-az-devops'
  $'(^|$|[^[:alnum:]_])az keyvault(^|$|[^[:alnum:]_])	nerf-az-keyvault'
  $'(^|$|[^[:alnum:]_])az monitor(^|$|[^[:alnum:]_])	nerf-az-monitor'
  $'(^|$|[^[:alnum:]_])az network(^|$|[^[:alnum:]_])	nerf-az-network'
  $'(^|$|[^[:alnum:]_])az pipelines(^|$|[^[:alnum:]_])	nerf-az-pipelines'
  $'(^|$|[^[:alnum:]_])az postgres(^|$|[^[:alnum:]_])	nerf-az-postgres'
  $'(^|$|[^[:alnum:]_])az repos(^|$|[^[:alnum:]_])	nerf-az-repos'
  $'(^|$|[^[:alnum:]_])az resource(^|$|[^[:alnum:]_])	nerf-az-resource'
  $'(^|$|[^[:alnum:]_])az group(^|$|[^[:alnum:]_])	nerf-az-resource'
  $'(^|$|[^[:alnum:]_])az role(^|$|[^[:alnum:]_])	nerf-az-role'
  $'(^|$|[^[:alnum:]_])az storage(^|$|[^[:alnum:]_])	nerf-az-storage'
  $'(^|$|[^[:alnum:]_])gh(^|$|[^[:alnum:]_])	nerf-gh'
  $'(^|$|[^[:alnum:]_])git(^|$|[^[:alnum:]_])	nerf-git'
  $'(^|$|[^[:alnum:]_])kubectl(^|$|[^[:alnum:]_])	nerf-kubectl'
  $'\\.nerftools/[^/]+/reports(^|$|[^[:alnum:]_])	nerf-report'
  $'(^|$|[^[:alnum:]_])nx(^|$|[^[:alnum:]_])	nerf-nx'
  $'(^|$|[^[:alnum:]_])cspell(^|$|[^[:alnum:]_])	nerf-pkgrun'
  $'(^|$|[^[:alnum:]_])markdownlint(^|$|[^[:alnum:]_])	nerf-pkgrun'
  $'(^|$|[^[:alnum:]_])prettier(^|$|[^[:alnum:]_])	nerf-pkgrun'
  $'(^|$|[^[:alnum:]_])terraform(^|$|[^[:alnum:]_])	nerf-tf'
  $'(^|$|[^[:alnum:]_])terragrunt(^|$|[^[:alnum:]_])	nerf-tg'
  $'(^|$|[^[:alnum:]_])uv(^|$|[^[:alnum:]_])	nerf-uv'
)

command -v jq >/dev/null 2>&1 || exit 0

_input=$(cat)
_tool=$(printf '%s' "$_input" | jq -r '.tool_name // empty' 2>/dev/null) || exit 0
[[ "$_tool" == "Bash" ]] || exit 0

_cmd=$(printf '%s' "$_input" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
[[ -n "$_cmd" ]] || exit 0

emit_deny() {
  jq -nc --arg r "$1" \
    '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $r}}'
}

# Find a version-aware sort (GNU `sort -V` or `gsort -V`). Returns "" on
# stdout if neither works.
_pick_version_sorter() {
  local probe_in=$'1.10.0\n1.9.0\n'
  local probe_out=$'1.9.0\n1.10.0'
  local cmd
  for cmd in sort gsort; do
    command -v "$cmd" >/dev/null 2>&1 || continue
    if [[ "$(printf '%s' "$probe_in" | "$cmd" -V 2>/dev/null)" == "$probe_out" ]]; then
      echo "$cmd"
      return 0
    fi
  done
  return 1
}

# POSIX-ERE escape a literal string so it can be safely interpolated into
# a regex. Escapes: . ^ $ * + ? | [ ] { } ( ) \
_ere_escape() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//./\\.}"
  s="${s//^/\\^}"
  s="${s//\$/\\$}"
  s="${s//\*/\\*}"
  s="${s//+/\\+}"
  s="${s//\?/\\?}"
  s="${s//|/\\|}"
  s="${s//[/\\[}"
  s="${s//]/\\]}"
  s="${s//\{/\\\{}"
  s="${s//\}/\\\}}"
  s="${s//(/\\(}"
  s="${s//)/\\)}"
  printf '%s' "$s"
}

# -- Check: current version --------------------------------------------------
#
# Self-derive this plugin's prefix + current version, then verify the
# command isn't invoking a tool path under a different version of OUR
# plugin. Method 1 (preferred): walk up from this hook's own path.
# Method 2 (fallback): scan ~/.claude/plugins/cache/*/<plugin>/ for the
# max version directory; refuses if multiple owners ship the same plugin
# name (the brand is supposed to disambiguate -- a name collision is a
# misconfig). Method 3: skip with warning.
_check_current_version() {
  case "${NERF_ENABLE_CURRENT_VERSION_HOOK:-}" in
    1 | [tT][rR][uU][eE] | [yY][eE][sS] | [oO][nN]) ;;
    *) return 0 ;;
  esac

  # Pick the sorter once. Used for both fallback max-version and the
  # older/newer categorization below.
  local vsort
  vsort=$(_pick_version_sorter) || vsort=""

  local self_path version_dir plugin_dir current_version=""
  self_path=$(realpath "$0" 2>/dev/null) || {
    echo "warning: nerf-pre-tool-use: cannot resolve own path; skipping current-version check" >&2
    return 0
  }
  version_dir=$(dirname "$(dirname "$self_path")")
  if [[ -d "$version_dir/skills" && -d "$version_dir/.claude-plugin" ]]; then
    plugin_dir=$(dirname "$version_dir")
    current_version=$(basename "$version_dir")
  else
    # Fallback: scan cache for any owner with our plugin name.
    if [[ -z "$vsort" ]]; then
      echo "warning: nerf-pre-tool-use: cannot self-derive plugin location and no version-aware sort available; skipping current-version check" >&2
      return 0
    fi
    plugin_dir=""
    local versions="" _p _vd owners_seen=0
    for _p in "$HOME/.claude/plugins/cache"/*/"$_PLUGIN_NAME"; do
      [[ -d "$_p" ]] || continue
      owners_seen=$((owners_seen + 1))
      plugin_dir="$_p"
      while IFS= read -r _vd; do
        [[ -d "$_vd" ]] && versions+="$(basename "$_vd")"$'\n'
      done < <(find "$_p" -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
    done
    if (( owners_seen > 1 )); then
      echo "warning: nerf-pre-tool-use: found 'nerftools' installed under multiple owners in $HOME/.claude/plugins/cache; brand should disambiguate -- a name collision is a misconfig; skipping current-version check" >&2
      return 0
    fi
    if [[ -z "$plugin_dir" || -z "$versions" ]]; then
      echo "warning: nerf-pre-tool-use: cannot determine plugin location; skipping current-version check" >&2
      return 0
    fi
    current_version=$(printf '%s' "$versions" | "$vsort" -V | tail -1)
  fi

  # Extract called versions from absolute paths in the command that match
  # OUR plugin tree and a brand-prefixed tool script. grep/sed/sort are
  # POSIX-standard; ungated for the same reason as cat/dirname above.
  local escaped_prefix escaped_brand path_re called=""
  escaped_prefix=$(_ere_escape "$plugin_dir/")
  escaped_brand=$(_ere_escape "$_BRAND")
  path_re="${escaped_prefix}[^/[:space:]]+/skills/[^/[:space:]]+/scripts/${escaped_brand}-[^[:space:]]+"
  called=$(printf '%s' "$_cmd" | grep -oE "$path_re" 2>/dev/null \
    | sed -E "s|^${escaped_prefix}([^/]+)/.*|\\1|" | sort -u)
  [[ -z "$called" ]] && return 0

  # Categorize ALL mismatches into older vs newer; prefer reporting newer
  # since that's the higher-signal condition (real config inconsistency,
  # not just a stale path).
  local v older_mismatch="" newer_mismatch=""
  while IFS= read -r v; do
    [[ -z "$v" ]] && continue
    [[ "$v" == "$current_version" ]] && continue
    if [[ -n "$vsort" ]] && [[ "$(printf '%s\n%s\n' "$v" "$current_version" | "$vsort" -V | tail -1)" == "$v" ]]; then
      newer_mismatch="$v"
    else
      # No sorter, OR vsort says older: treat as older (safer fallback).
      older_mismatch="$v"
    fi
  done <<< "$called"
  [[ -z "$older_mismatch" && -z "$newer_mismatch" ]] && return 0

  local direction mismatch
  if [[ -n "$newer_mismatch" ]]; then
    direction="newer"
    mismatch="$newer_mismatch"
  else
    direction="older"
    mismatch="$older_mismatch"
  fi

  local msg
  if [[ "$direction" == "older" ]]; then
    msg="You invoked an older version of this plugin's tools (called: ${mismatch}, current: ${current_version}). You must use the current version ${current_version}.

If you suspect a configuration or plugin-install problem, stop and report this to the user immediately and await instructions.

Otherwise, if the latest version's tools appear broken or are missing functionality you need, consider filing a nerf-report (if the skill is available), or stop and report directly to the user and await instructions.

Do not attempt to work around this. Work within the provided tools and escalate to the user if things seem off."
  else
    msg="You invoked a NEWER version of this plugin's tools than is currently installed (called: ${mismatch}, installed: ${current_version}). This indicates a serious configuration inconsistency.

Stop immediately. Report this condition to the user and await instructions.

Do not attempt to work around this."
  fi

  emit_deny "$msg"
  exit 0
}

# -- Check: bash hint --------------------------------------------------------

_check_bash_hint() {
  case "${NERF_ENABLE_BASH_HINT_HOOK:-}" in
    1 | [tT][rR][uU][eE] | [yY][eE][sS] | [oO][nN]) ;;
    *) return 0 ;;
  esac

  # This is a UX nudge toward the wrappers, not a security boundary.
  # The actual security gate is the user's permission system; if this
  # detection misses, the raw command still runs through normal
  # permission-based execution. The tradeoff is asymmetric: false
  # positives (firing on legitimate wrapper use) cause real friction;
  # false negatives (missing a raw command) cost only the nudge. So
  # the scan is intentionally lenient.
  #
  # Skip the redirect when the command is invoking a wrapper itself.
  # Split on shell separators (&&, ||, ;, |, &) into segments; in each
  # segment, skip leading "VAR=val" env-var assignments, then check the
  # leading command's basename. If it starts with the wrapper prefix,
  # it's a wrapper call. If it's a known command-delegating runner
  # (timeout, nice, time, env, ionice, chrt), scan the rest of the
  # segment for any wrapper-prefixed basename. The scan does NOT try
  # to parse the runner's argv, so an agent can short-circuit the
  # nudge by appending a `nerf-`-prefixed token -- that's fine, the
  # underlying command still hits the permission layer. `sudo` is
  # excluded from the runner allowlist because `sudo nerf-foo` is
  # unusual enough that we'd rather the agent acknowledge it via the
  # bypass sentinel.
  if [[ -n "$_WRAPPER_PREFIX" ]]; then
    local _norm="${_cmd//&&/$'\n'}"
    _norm="${_norm//||/$'\n'}"
    _norm="${_norm//[|;&]/$'\n'}"
    local _seg _toks _base _i
    while IFS= read -r _seg; do
      _seg="${_seg#"${_seg%%[![:space:]]*}"}"
      [[ -z "$_seg" ]] && continue
      read -r -a _toks <<< "$_seg"
      _i=0
      while (( _i < ${#_toks[@]} )) && [[ "${_toks[$_i]}" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do
        ((_i++))
      done
      (( _i >= ${#_toks[@]} )) && continue
      _base="${_toks[$_i]##*/}"
      if [[ "$_base" == "$_WRAPPER_PREFIX"* ]]; then
        return 0
      fi
      case "$_base" in
        timeout|nice|time|env|ionice|chrt)
          ((_i++))
          while (( _i < ${#_toks[@]} )); do
            if [[ "${_toks[$_i]##*/}" == "$_WRAPPER_PREFIX"* ]]; then
              return 0
            fi
            ((_i++))
          done
          ;;
      esac
    done <<< "$_norm"
  fi

  # Bypass sentinel for THIS check only. Other checks (e.g. current-version)
  # are not bypassable via this sentinel.
  local _bypass_re="# ${_BRAND_RE}:bypass-bash-hint[[:space:]]+[^[:space:]]"
  if [[ "$_cmd" =~ $_bypass_re ]]; then
    return 0
  fi

  local _matched=() _row _pat _skill _already _m
  for _row in "${_PATTERNS[@]:-}"; do
    _pat="${_row%%$'\t'*}"
    _skill="${_row##*$'\t'}"
    _already=0
    for _m in "${_matched[@]:-}"; do
      if [[ "$_m" == "$_skill" ]]; then _already=1; break; fi
    done
    [[ $_already -eq 1 ]] && continue
    if [[ "$_cmd" =~ $_pat ]]; then
      _matched+=("$_skill")
    fi
  done

  [[ ${#_matched[@]} -eq 0 ]] && return 0

  local _list="" _s
  for _s in "${_matched[@]}"; do
    if [[ -z "$_list" ]]; then _list="\`${_s}\`"; else _list="${_list}, \`${_s}\`"; fi
  done

  local _msg="The following ${_BRAND} skill(s) may wrap this command: ${_list}.

Use one if it covers what you need. The detection is heuristic and can be wrong on unusual command shapes -- if you think it is wrong, that is itself a valid bypass reason. To run the command directly anyway:
1. File a \`nerf-report\` explaining what you needed (or why this hint looks like a false positive).
2. Retry the command with the resulting report filename appended as \`# ${_BRAND}:bypass-bash-hint <report-filename>\`."

  emit_deny "$_msg"
  exit 0
}

# Run in declaration order; each denies-and-exits on its own.
_check_current_version
_check_bash_hint
