#!/usr/bin/env bash
set -euo pipefail

VERSION="2.0.4"

CONFIG_FILE="${CLAUDE_GUARD_CONFIG:-${SAFE_CLAUDE_CONFIG:-$HOME/.safe-claude-official.json}}"
OFFICIAL_SETTINGS_OVERRIDE="${CLAUDE_GUARD_SETTINGS:-}"
OFFICIAL_CONFIG_DIR=""
OFFICIAL_SETTINGS=""
CA_CERT_FILE="${CLAUDE_GUARD_CA_CERT:-/etc/ssl/cert.pem}"
CLAUDE_PROXY_URL="${CLAUDE_GUARD_PROXY:-${CLAUDE_OFFICIAL_PROXY:-http://127.0.0.1:7897}}"
NO_PROXY_LIST="${CLAUDE_GUARD_NO_PROXY:-${CLAUDE_OFFICIAL_NO_PROXY:-127.0.0.1,localhost,::1,.local}}"
IP_CHECK_RETRIES="${CLAUDE_GUARD_IP_RETRIES:-${CLAUDE_OFFICIAL_IP_RETRIES:-3}}"
IP_CHECK_URLS="${CLAUDE_GUARD_IP_CHECK_URLS:-${CLAUDE_OFFICIAL_IP_CHECK_URLS:-https://ipinfo.io/ip https://api.ipify.org}}"
TLS_CHECK_RETRIES="${CLAUDE_GUARD_TLS_RETRIES:-${CLAUDE_OFFICIAL_TLS_RETRIES:-3}}"
PLATFORM_TLS_MODE="${CLAUDE_GUARD_PLATFORM_TLS_MODE:-${CLAUDE_OFFICIAL_PLATFORM_TLS_MODE:-warn}}"
ASSUME_YES="${CLAUDE_GUARD_ASSUME_YES:-0}"
WATCHDOG_ENABLED="${CLAUDE_GUARD_WATCHDOG:-1}"
WATCHDOG_DRY_RUN="${CLAUDE_GUARD_WATCHDOG_DRY_RUN:-1}"
IP_INTERVAL="${CLAUDE_GUARD_IP_INTERVAL:-60}"
API_INTERVAL="${CLAUDE_GUARD_API_INTERVAL:-180}"
RECOVERY_INTERVAL="${CLAUDE_GUARD_RECOVERY_INTERVAL:-30}"
WATCHDOG_TICK_SECONDS="${CLAUDE_GUARD_WATCHDOG_TICK_SECONDS:-5}"
WAKE_GAP_SECONDS="${CLAUDE_GUARD_WAKE_GAP_SECONDS:-120}"
IP_BAD_THRESHOLD="${CLAUDE_GUARD_IP_BAD_THRESHOLD:-2}"
API_BAD_THRESHOLD="${CLAUDE_GUARD_API_BAD_THRESHOLD:-3}"
RESUME_GOOD_THRESHOLD="${CLAUDE_GUARD_RESUME_GOOD_THRESHOLD:-2}"
KILL_AFTER_PAUSE_SECONDS="${CLAUDE_GUARD_KILL_AFTER_PAUSE_SECONDS:-}"
LOG_FILE="${CLAUDE_GUARD_LOG_FILE:-$HOME/.claude-guard/guard.log}"
LOG_MAX_BYTES="${CLAUDE_GUARD_LOG_MAX_BYTES:-1048576}"
NOTIFY_OVERRIDE_SET=0
if [ "${CLAUDE_GUARD_NOTIFY+x}" = "x" ]; then
  NOTIFY_OVERRIDE_SET=1
fi
NOTIFY_ENABLED="${CLAUDE_GUARD_NOTIFY:-0}"
DRY_RUN_STDERR="${CLAUDE_GUARD_DRY_RUN_STDERR:-0}"
FINGERPRINT_MODE="${CLAUDE_GUARD_FINGERPRINT_MODE:-fail-active}"
LEGACY_PROFILE_MODE="${CLAUDE_GUARD_LEGACY_PROFILE_MODE:-warn}"
SETTINGS_RISK_RE='ANTHROPIC_(BASE_URL|AUTH_TOKEN|API_KEY|CUSTOM_HEADERS|AWS_BASE_URL|BEDROCK_BASE_URL|VERTEX_BASE_URL|FOUNDRY_BASE_URL)|apiKeyHelper|PROXY_MANAGED|CLAUDE_CODE_USE_(GATEWAY|BEDROCK|VERTEX|FOUNDRY|ANTHROPIC_AWS|MANTLE)|CLAUDE_CODE_PROCESS_WRAPPER|processWrapper|CLAUDE_CODE_RETRY_WATCHDOG|CLAUDE_CODE_ATTRIBUTION_HEADER|HTTP_PROXY|HTTPS_PROXY|ALL_PROXY|NODE_EXTRA_CA_CERTS|SSL_CERT_FILE|CLAUDE_CODE_CERT_STORE|CLAUDE_CONFIG_DIR'
PRECHECK_ONLY=0
COMMAND_MODE="run"
STATUS_JSON=0
UI_MODE="${CLAUDE_GUARD_UI:-auto}"
UI_ENABLED=0
UI_DYNAMIC=0
UI_UNICODE=0
UI_TOTAL=8
UI_INDEX=0
UI_CURRENT_STEP=""
UI_STEP_NOTES=""
UI_STEP_WARNINGS=""
UI_STEP_WARNING_COUNT=0
UI_WARNING_TOTAL=0
UI_STARTED=0
UI_COMPLETE=0
UI_STARTED_AT=0
UI_RED=""
UI_GREEN=""
UI_YELLOW=""
UI_CYAN=""
UI_DIM=""
UI_RESET=""

usage() {
  cat <<'EOF'
Claude 守护 - startup guard for official Claude Code

Usage:
  claude-guard [--precheck-only] [claude args...]
  claude-guard status [--json]
  claude-guard doctor
  claude-guard diagnose
  claude-guard --version
  claude-guard --help

Environment:
  CLAUDE_GUARD_CONFIG        Safe config JSON. Default: ~/.safe-claude-official.json
  CLAUDE_GUARD_SETTINGS      Override the settings file from config_dir.
  CLAUDE_GUARD_PROXY         Required proxy URL. Default: http://127.0.0.1:7897
  CLAUDE_GUARD_NO_PROXY      NO_PROXY list. Default: 127.0.0.1,localhost,::1,.local
  CLAUDE_GUARD_ASSUME_YES    Set to 1 to skip the final Enter prompt when IP is allowed.
  CLAUDE_GUARD_IP_CHECK_URLS Space-separated IPv4 echo endpoints.
  CLAUDE_GUARD_UI             auto, always, or never. Default: auto.
                              auto renders the visual preflight only in an interactive terminal.
  NO_COLOR                   Disable ANSI colors while keeping the visual preflight layout.
  CLAUDE_GUARD_WATCHDOG      Set to 0 to disable runtime guarding. Default: 1
  CLAUDE_GUARD_WATCHDOG_DRY_RUN
                              Default: 1. Logs would-pause/would-resume actions only.
  CLAUDE_GUARD_RECOVERY_INTERVAL
                              Seconds between paired IP/API probes while degraded. Default: 30.
  CLAUDE_GUARD_DRY_RUN_STDERR
                              Set to 1 to print dry-run pause/resume notices to stderr.
  CLAUDE_GUARD_FINGERPRINT_MODE
                              off, warn, fail-active, or strict. Default: fail-active.
  CLAUDE_GUARD_LEGACY_PROFILE_MODE
                              off, warn, or strict for ~/.claude contamination. Default: warn.
  CLAUDE_GUARD_LOG_FILE      Default: ~/.claude-guard/guard.log
  CLAUDE_GUARD_LOG_MAX_BYTES Rotate to guard.log.1 after this size. Default: 1048576.
  CLAUDE_GUARD_NOTIFY        Set to 1 for macOS pause/resume notifications.
                              If unset, reads boolean `notify` from the safe config. Default: off.

Lifecycle policy:
  Background agents, background tasks, cron, workflows, Remote Control,
  deep-link registration, and automatic updates must be disabled in the
  official settings file. Project settings and CLI flags cannot override
  the guarded settings or official Anthropic route.

The config file must contain:
  {
    "command": "/absolute/path/to/original/claude",
    "config_dir": "/absolute/path/to/versioned/profile",
    "allowed_ips": ["x.x.x.x"],
    "allowed_cidrs": ["x.x.x.0/24"]
  }
EOF
}

case "${1:-}" in
  status)
    COMMAND_MODE="status"
    shift
    if [ "${1:-}" = "--json" ]; then
      STATUS_JSON=1
      shift
    fi
    if [ "$#" -gt 0 ]; then
      printf 'status 不接受额外参数: %s\n' "$*" >&2
      exit 2
    fi
    ;;
  doctor)
    COMMAND_MODE="doctor"
    PRECHECK_ONLY=1
    shift
    if [ "$#" -gt 0 ]; then
      printf 'doctor 不接受额外参数: %s\n' "$*" >&2
      exit 2
    fi
    ;;
  diagnose)
    COMMAND_MODE="diagnose"
    shift
    if [ "$#" -gt 0 ]; then
      printf 'diagnose 不接受额外参数: %s\n' "$*" >&2
      exit 2
    fi
    ;;
esac

while [ "$#" -gt 0 ]; do
  case "$1" in
    --version)
      printf 'claude-guard %s\n' "$VERSION"
      exit 0
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    --precheck-only)
      PRECHECK_ONLY=1
      shift
      ;;
    --)
      shift
      break
      ;;
    *)
      break
      ;;
  esac
done

require_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    printf '缺少依赖命令: %s\n' "$1" >&2
    case "$1" in
      curl) printf '安装建议: macOS 通常自带 curl；如缺失可执行 brew install curl\n' >&2 ;;
      jq) printf '安装建议: brew install jq\n' >&2 ;;
    esac
    exit 127
  fi
}

require_cmd jq
case "$COMMAND_MODE" in
  status|diagnose) ;;
  *) require_cmd curl ;;
esac

configure_ui() {
  case "$UI_MODE" in
    auto)
      if [ -t 2 ] && [ "${TERM:-}" != "dumb" ]; then
        UI_ENABLED=1
      fi
      ;;
    always)
      UI_ENABLED=1
      ;;
    never)
      UI_ENABLED=0
      ;;
    *)
      printf 'CLAUDE_GUARD_UI 只能是 auto、always 或 never，当前是: %s\n' "$UI_MODE" >&2
      exit 2
      ;;
  esac

  if [ "$UI_ENABLED" -eq 1 ] && [ -t 2 ] && [ "${TERM:-}" != "dumb" ]; then
    UI_DYNAMIC=1
  fi

  if [ "$UI_ENABLED" -eq 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then
    UI_RED=$'\033[31m'
    UI_GREEN=$'\033[32m'
    UI_YELLOW=$'\033[33m'
    UI_CYAN=$'\033[36m'
    UI_DIM=$'\033[2m'
    UI_RESET=$'\033[0m'
  fi

  case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
    *[Uu][Tt][Ff]-8*|*[Uu][Tt][Ff]8*) UI_UNICODE=1 ;;
  esac
}

ui_progress_bar() {
  local completed="$1"
  local width=16
  local filled empty i bar=""

  filled=$((completed * width / UI_TOTAL))
  empty=$((width - filled))
  i=0
  while [ "$i" -lt "$filled" ]; do
    if [ "$UI_UNICODE" -eq 1 ]; then
      bar="${bar}█"
    else
      bar="${bar}#"
    fi
    i=$((i + 1))
  done
  i=0
  while [ "$i" -lt "$empty" ]; do
    if [ "$UI_UNICODE" -eq 1 ]; then
      bar="${bar}░"
    else
      bar="${bar}-"
    fi
    i=$((i + 1))
  done
  printf '%s' "$bar"
}

ui_icon() {
  local kind="$1"

  if [ "$UI_UNICODE" -eq 1 ]; then
    case "$kind" in
      pass) printf '✓' ;;
      warn) printf '!' ;;
      fail) printf '×' ;;
      active) printf '…' ;;
    esac
  else
    case "$kind" in
      pass) printf 'OK' ;;
      warn) printf '!' ;;
      fail) printf 'X' ;;
      active) printf '..' ;;
    esac
  fi
}

ui_header() {
  local title_separator="-"

  [ "$UI_ENABLED" -eq 1 ] || return 0
  [ "$UI_UNICODE" -eq 1 ] && title_separator="·"
  UI_STARTED=1
  UI_STARTED_AT="$SECONDS"
  printf '\n%sClaude Guard %s Official Preflight%s  %sv%s%s\n' \
    "$UI_CYAN" "$title_separator" "$UI_RESET" "$UI_DIM" "$VERSION" "$UI_RESET" >&2
  printf '%s  profile  %s%s\n' "$UI_DIM" "$OFFICIAL_CONFIG_DIR" "$UI_RESET" >&2
  printf '%s  route    %s%s\n\n' "$UI_DIM" "$CLAUDE_PROXY_URL" "$UI_RESET" >&2
  trap 'ui_exit_trap "$?"' EXIT
}

ui_step_start() {
  local label="$1"
  local bar icon

  UI_INDEX=$((UI_INDEX + 1))
  UI_CURRENT_STEP="$label"
  UI_STEP_NOTES=""
  UI_STEP_WARNINGS=""
  UI_STEP_WARNING_COUNT=0
  [ "$UI_ENABLED" -eq 1 ] || return 0

  bar="$(ui_progress_bar "$UI_INDEX")"
  icon="$(ui_icon active)"
  printf '  %s%s%s  [%s] %s/%s  %s\n' \
    "$UI_CYAN" "$icon" "$UI_RESET" "$bar" "$UI_INDEX" "$UI_TOTAL" "$label" >&2
}

ui_note() {
  local format="$1"
  local message
  shift

  # shellcheck disable=SC2059
  printf -v message "$format" "$@"
  if [ "$UI_ENABLED" -eq 1 ]; then
    if [ -n "$UI_STEP_NOTES" ]; then
      UI_STEP_NOTES="${UI_STEP_NOTES}"$'\n'"$message"
    else
      UI_STEP_NOTES="$message"
    fi
  else
    printf '%s\n' "$message"
  fi
}

ui_visual_note() {
  [ "$UI_ENABLED" -eq 1 ] || return 0
  ui_note "$@"
}

ui_warn() {
  local format="$1"
  local message
  shift

  # shellcheck disable=SC2059
  printf -v message "$format" "$@"
  if [ "$UI_ENABLED" -eq 1 ]; then
    if [ -n "$UI_STEP_WARNINGS" ]; then
      UI_STEP_WARNINGS="${UI_STEP_WARNINGS}"$'\n'"$message"
    else
      UI_STEP_WARNINGS="$message"
    fi
    UI_STEP_WARNING_COUNT=$((UI_STEP_WARNING_COUNT + 1))
    UI_WARNING_TOTAL=$((UI_WARNING_TOTAL + 1))
  else
    printf '%s\n' "$message" >&2
  fi
}

ui_render_detail() {
  local kind="$1"
  local content="$2"
  local color="$UI_DIM"
  local prefix=" "
  local line

  [ -n "$content" ] || return 0
  if [ "$kind" = "warn" ]; then
    color="$UI_YELLOW"
    prefix="!"
  fi
  while IFS= read -r line; do
    [ -n "$line" ] || continue
    printf '      %s%s %s%s\n' "$color" "$prefix" "$line" "$UI_RESET" >&2
  done <<EOF
$content
EOF
}

ui_step_finish() {
  local bar icon color status="pass"

  [ "$UI_ENABLED" -eq 1 ] || return 0
  if [ "$UI_STEP_WARNING_COUNT" -gt 0 ]; then
    status="warn"
  fi

  bar="$(ui_progress_bar "$UI_INDEX")"
  icon="$(ui_icon "$status")"
  color="$UI_GREEN"
  [ "$status" = "warn" ] && color="$UI_YELLOW"

  if [ "$UI_DYNAMIC" -eq 1 ]; then
    printf '\033[1A\r\033[2K' >&2
  fi
  printf '  %s%s%s  [%s] %s/%s  %s\n' \
    "$color" "$icon" "$UI_RESET" "$bar" "$UI_INDEX" "$UI_TOTAL" "$UI_CURRENT_STEP" >&2
  ui_render_detail note "$UI_STEP_NOTES"
  ui_render_detail warn "$UI_STEP_WARNINGS"
}

ui_finish() {
  local elapsed=0
  local icon message

  UI_COMPLETE=1
  [ "$UI_ENABLED" -eq 1 ] || return 0
  elapsed=$((SECONDS - UI_STARTED_AT))
  icon="$(ui_icon pass)"
  message="预检已通过"
  if [ "$UI_WARNING_TOTAL" -gt 0 ]; then
    message="${message}，含 ${UI_WARNING_TOTAL} 条警告"
  fi
  printf '\n  %s%s  %s%s  %s8/8%s  %s%ss%s\n' \
    "$UI_GREEN" "$icon" "$message" "$UI_RESET" \
    "$UI_DIM" "$UI_RESET" "$UI_DIM" "$elapsed" "$UI_RESET" >&2
}

ui_exit_trap() {
  local status="$1"
  local icon

  if [ "$UI_ENABLED" -eq 1 ] && [ "$UI_STARTED" -eq 1 ] &&
     [ "$UI_COMPLETE" -ne 1 ] && [ "$status" -ne 0 ]; then
    icon="$(ui_icon fail)"
    printf '\n  %s%s  预检未通过%s  %s%s/%s · %s · exit=%s%s\n' \
      "$UI_RED" "$icon" "$UI_RESET" "$UI_DIM" "$UI_INDEX" "$UI_TOTAL" \
      "${UI_CURRENT_STEP:-初始化}" "$status" "$UI_RESET" >&2
  fi
}

configure_ui

resolve_path() {
  local path="$1"

  if command -v realpath >/dev/null 2>&1; then
    realpath "$path" 2>/dev/null || printf '%s\n' "$path"
  else
    printf '%s\n' "$path"
  fi
}

clean_env() {
  env -i \
    HOME="$HOME" \
    USER="${USER:-user}" \
    PATH="$PATH" \
    TERM="${TERM:-xterm-256color}" \
    LANG="${LANG:-en_US.UTF-8}" \
    LC_ALL="${LC_ALL:-en_US.UTF-8}" \
    SHELL="${SHELL:-/bin/zsh}" \
    "$@"
}

official_net_env() {
  env -i \
    HOME="$HOME" \
    USER="${USER:-user}" \
    PATH="$PATH" \
    TERM="${TERM:-xterm-256color}" \
    LANG="${LANG:-en_US.UTF-8}" \
    LC_ALL="${LC_ALL:-en_US.UTF-8}" \
    SHELL="${SHELL:-/bin/zsh}" \
    HTTP_PROXY="$CLAUDE_PROXY_URL" \
    HTTPS_PROXY="$CLAUDE_PROXY_URL" \
    ALL_PROXY="$CLAUDE_PROXY_URL" \
    http_proxy="$CLAUDE_PROXY_URL" \
    https_proxy="$CLAUDE_PROXY_URL" \
    all_proxy="$CLAUDE_PROXY_URL" \
    NO_PROXY="$NO_PROXY_LIST" \
    no_proxy="$NO_PROXY_LIST" \
    SSL_CERT_FILE="$CA_CERT_FILE" \
    NODE_EXTRA_CA_CERTS="$CA_CERT_FILE" \
    "$@"
}

sha256_file() {
  local file="$1"

  if command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$file" | awk '{print $1}'
    return 0
  fi
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$file" | awk '{print $1}'
    return 0
  fi

  printf '缺少 SHA-256 校验工具；需要 shasum 或 sha256sum。\n' >&2
  return 1
}

check_client_identity() {
  local claude_bin="$1"
  local resolved_bin expected_version expected_sha expected_team
  local actual_version actual_sha signature team

  resolved_bin="$(resolve_path "$claude_bin")"
  expected_version="$(jq -r '.client_version // empty' "$CONFIG_FILE")"
  expected_sha="$(jq -r '.client_sha256 // empty' "$CONFIG_FILE")"
  expected_team="$(jq -r '.client_macos_team_id // empty' "$CONFIG_FILE")"

  if [ -n "$expected_version" ]; then
    actual_version="$(clean_env DISABLE_UPDATES=1 "$resolved_bin" --version 2>/dev/null | awk 'NR == 1 {print $1}')"
    if [ "$actual_version" != "$expected_version" ]; then
      printf '客户端版本校验失败：期望 %s，实际 %s。\n' "$expected_version" "${actual_version:-unknown}" >&2
      exit 12
    fi
  else
    actual_version="unpinned"
  fi

  if [ -n "$expected_sha" ]; then
    actual_sha="$(sha256_file "$resolved_bin")"
    if [ "$actual_sha" != "$expected_sha" ]; then
      printf '客户端 SHA-256 校验失败：%s\n' "$resolved_bin" >&2
      exit 12
    fi
  else
    actual_sha="unpinned"
  fi

  if [ -n "$expected_team" ]; then
    if [ "$(uname -s)" != "Darwin" ] || ! command -v codesign >/dev/null 2>&1; then
      printf '客户端签名校验失败：配置要求 macOS Team ID，但当前平台无法使用 codesign。\n' >&2
      exit 12
    fi
    if ! codesign --verify --strict "$resolved_bin" >/dev/null 2>&1; then
      printf '客户端签名校验失败：codesign 未通过: %s\n' "$resolved_bin" >&2
      exit 12
    fi
    signature="$(codesign -dv --verbose=4 "$resolved_bin" 2>&1 || true)"
    team="$(printf '%s\n' "$signature" | sed -n 's/^TeamIdentifier=//p' | head -1)"
    if [ "$team" != "$expected_team" ]; then
      printf '客户端签名校验失败：期望 Team ID %s，实际 %s。\n' "$expected_team" "${team:-unknown}" >&2
      exit 12
    fi
  else
    team="unpinned"
  fi

  ui_note '客户端身份校验通过：version=%s sha256=%s team=%s' \
    "$actual_version" "${actual_sha:0:12}" "$team"
}

validate_json_file() {
  local file="$1"
  local label="$2"

  if ! jq empty "$file" >/dev/null 2>&1; then
    printf '%s 不是有效 JSON，已拒绝启动: %s\n' "$label" "$file" >&2
    exit 5
  fi
}

settings_risk_paths() {
  local file="$1"

  jq -r --arg re "$SETTINGS_RISK_RE" '
    paths(scalars) as $p
    | ($p | map(tostring) | join(".")) as $path
    | (getpath($p) | tostring) as $value
    | select(
        ($path | test($re; "i"))
        or ($value | test("127\\.0\\.0\\.1:15721|PROXY_MANAGED"; "i"))
      )
    | $path
  ' "$file" | sort -u
}

check_blocked_plugins() {
  local file="$1"
  local label="$2"
  local plugin

  while IFS= read -r plugin; do
    [ -n "$plugin" ] || continue
    if jq -e --arg plugin "$plugin" '(.enabledPlugins // {})[$plugin] == true' "$file" >/dev/null; then
      printf '%s 启用了会脱离终端运行的受阻插件，已拒绝启动: %s (%s)\n' \
        "$label" "$plugin" "$file" >&2
      exit 13
    fi
  done <<EOF
$(jq -r '.blocked_plugins // [] | .[] | select(type == "string")' "$CONFIG_FILE")
EOF
}

check_official_settings_policy() {
  local missing risk_paths pinned_model

  validate_json_file "$OFFICIAL_SETTINGS" "官方 settings"

  risk_paths="$(settings_risk_paths "$OFFICIAL_SETTINGS")"
  if [ -n "$risk_paths" ]; then
    printf '官方 settings 含有路由、凭据、证书或重试策略改写，已拒绝启动: %s\n' "$OFFICIAL_SETTINGS" >&2
    printf '%s\n' "$risk_paths" | sed 's/^/  - /' >&2
    exit 5
  fi

  if jq -e '.require_unpinned_model // false' "$CONFIG_FILE" >/dev/null; then
    pinned_model="$(jq -r '.model // empty' "$OFFICIAL_SETTINGS")"
    if [ -n "$pinned_model" ]; then
      printf '官方 settings 固定 model=%s，已拒绝启动；请让新版客户端自行选择模型或使用一次性的 --model。\n' \
        "$pinned_model" >&2
      exit 6
    fi
  fi

  missing="$(jq -r '
    [
      if .disableAgentView != true then "disableAgentView=true" else empty end,
      if .disableRemoteControl != true then "disableRemoteControl=true" else empty end,
      if .disableDeepLinkRegistration != "disable" then "disableDeepLinkRegistration=disable" else empty end,
      if .disableAllHooks != true then "disableAllHooks=true" else empty end,
      if .disableWorkflows != true then "disableWorkflows=true" else empty end,
      if ((.env // {}).DISABLE_UPDATES != "1") then "env.DISABLE_UPDATES=1" else empty end,
      if ((.env // {}).CLAUDE_CODE_DISABLE_AGENT_VIEW != "1") then "env.CLAUDE_CODE_DISABLE_AGENT_VIEW=1" else empty end,
      if ((.env // {}).CLAUDE_CODE_DISABLE_BACKGROUND_TASKS != "1") then "env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1" else empty end,
      if ((.env // {}).CLAUDE_CODE_DISABLE_CRON != "1") then "env.CLAUDE_CODE_DISABLE_CRON=1" else empty end,
      if ((.env // {}).CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF != "1") then "env.CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF=1" else empty end,
      if ((.env // {}).CLAUDE_DISABLE_ADOPT != "1") then "env.CLAUDE_DISABLE_ADOPT=1" else empty end,
      if ((.env // {}).CLAUDE_CODE_DISABLE_WORKFLOWS != "1") then "env.CLAUDE_CODE_DISABLE_WORKFLOWS=1" else empty end,
      if ((.env // {}).CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS != "0") then "env.CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS=0" else empty end,
      if ((.env // {}).CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH != "1") then "env.CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1" else empty end,
      if ((.env // {}).CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS != "3") then "env.CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS=3" else empty end,
      if (((.env // {}).CLAUDE_AUTO_BACKGROUND_TASKS // "") == "1") then "remove env.CLAUDE_AUTO_BACKGROUND_TASKS=1" else empty end,
      if (((.env // {}).CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS // "") == "1") then "remove env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1" else empty end,
      if (((.env // {}).CLAUDE_CODE_RETRY_WATCHDOG // "") != "") then "remove env.CLAUDE_CODE_RETRY_WATCHDOG" else empty end
    ]
    | .[]
  ' "$OFFICIAL_SETTINGS")"

  if [ -n "$missing" ]; then
    printf '官方 settings 未满足 v%s 生命周期策略，已拒绝启动: %s\n' "$VERSION" "$OFFICIAL_SETTINGS" >&2
    printf '%s\n' "$missing" | sed 's/^/  - /' >&2
    exit 13
  fi

  check_blocked_plugins "$OFFICIAL_SETTINGS" "官方 settings"
  ui_note '生命周期策略通过：后台会话、后台任务、排程、工作流、Remote Control 与自动更新均已关闭。'
}

project_settings_files() {
  local dir="$PWD"
  local file

  while [ -n "$dir" ] && [ "$dir" != "$HOME" ]; do
    for file in "$dir/.claude/settings.json" "$dir/.claude/settings.local.json"; do
      [ -f "$file" ] && printf '%s\n' "$file"
    done
    [ "$dir" = "/" ] && break
    dir="$(dirname "$dir")"
  done
}

check_project_settings_policy() {
  local file risk_paths unsafe

  while IFS= read -r file; do
    [ -n "$file" ] || continue
    validate_json_file "$file" "项目设置"

    risk_paths="$(settings_risk_paths "$file")"
    if [ -n "$risk_paths" ]; then
      printf '项目设置试图改写官方路由、凭据、证书或重试策略，已拒绝启动: %s\n' "$file" >&2
      printf '%s\n' "$risk_paths" | sed 's/^/  - /' >&2
      exit 13
    fi

    unsafe="$(jq -r '
      [
        if (.disableAgentView != null and .disableAgentView != true) then "disableAgentView" else empty end,
        if (.disableRemoteControl != null and .disableRemoteControl != true) then "disableRemoteControl" else empty end,
        if (.disableDeepLinkRegistration != null and .disableDeepLinkRegistration != "disable") then "disableDeepLinkRegistration" else empty end,
        if (.disableAllHooks != null and .disableAllHooks != true) then "disableAllHooks" else empty end,
        if (.disableWorkflows != null and .disableWorkflows != true) then "disableWorkflows" else empty end,
        if (((.env // {}).CLAUDE_CODE_DISABLE_AGENT_VIEW // "1") != "1") then "env.CLAUDE_CODE_DISABLE_AGENT_VIEW" else empty end,
        if (((.env // {}).CLAUDE_CODE_DISABLE_BACKGROUND_TASKS // "1") != "1") then "env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS" else empty end,
        if (((.env // {}).CLAUDE_CODE_DISABLE_CRON // "1") != "1") then "env.CLAUDE_CODE_DISABLE_CRON" else empty end,
        if (((.env // {}).CLAUDE_CODE_DISABLE_WORKFLOWS // "1") != "1") then "env.CLAUDE_CODE_DISABLE_WORKFLOWS" else empty end,
        if (((.env // {}).CLAUDE_AUTO_BACKGROUND_TASKS // "") == "1") then "env.CLAUDE_AUTO_BACKGROUND_TASKS" else empty end,
        if (((.env // {}).CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS // "") == "1") then "env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" else empty end
      ]
      | .[]
    ' "$file")"
    if [ -n "$unsafe" ]; then
      printf '项目设置试图降低生命周期保护，已拒绝启动: %s\n' "$file" >&2
      printf '%s\n' "$unsafe" | sed 's/^/  - /' >&2
      exit 13
    fi

    check_blocked_plugins "$file" "项目设置"
  done <<EOF
$(project_settings_files)
EOF
}

check_cli_policy() {
  local arg

  for arg in "$@"; do
    case "$arg" in
      --settings|--settings=*)
        printf '官方入口不允许覆盖 --settings；请修改受守护的官方 settings 文件。\n' >&2
        exit 13
        ;;
      --setting-sources|--setting-sources=*)
        printf '官方入口不允许覆盖 --setting-sources。\n' >&2
        exit 13
        ;;
      --bg|--bg=*|--background|--background=*|agents|attach|respawn)
        printf '后台会话已禁用；请在当前前台终端中运行 Claude。\n' >&2
        exit 13
        ;;
      --tmux|--tmux=*)
        printf '受守护入口不允许启动脱离当前终端的 tmux/iTerm2 会话。\n' >&2
        exit 13
        ;;
      --remote-control|--remote-control=*|remote-control)
        printf 'Remote Control 已禁用；官方 Claude 必须留在受守护的前台终端中。\n' >&2
        exit 13
        ;;
    esac
  done
}

check_model_policy() {
  local expect_model=0
  local arg model

  for arg in "$@"; do
    model=""
    if [ "$expect_model" -eq 1 ]; then
      model="$arg"
      expect_model=0
    else
      case "$arg" in
        --model)
          expect_model=1
          ;;
        --model=*)
          model="${arg#--model=}"
          ;;
      esac
    fi

    [ -n "$model" ] || continue
    if jq -e --arg model "$model" \
      '(.blocked_models // []) | index($model)' "$CONFIG_FILE" >/dev/null; then
      printf '模型 %s 位于本机 blocked_models 清单，已拒绝启动。\n' "$model" >&2
      exit 6
    fi
  done
}

check_tls_host() {
  local host="$1"
  local mode="${2:-required}"
  local out issuer reason attempt

  attempt=1
  while [ "$attempt" -le "$TLS_CHECK_RETRIES" ]; do
    reason=''
    if out="$(official_net_env curl -4 -x "$CLAUDE_PROXY_URL" -vI --connect-timeout 8 --max-time 15 "https://${host}/" 2>&1 >/dev/null)"; then
      if ! printf '%s\n' "$out" | grep -Eq "CONNECT ${host}:443|CONNECT tunnel established|CONNECT phase completed"; then
        reason="${host} 没有走 ${CLAUDE_PROXY_URL} 的 HTTP CONNECT 隧道"
      elif ! printf '%s\n' "$out" | grep -q 'SSL certificate verify ok'; then
        reason="${host} 未显示证书验证通过"
      else
        issuer="$(printf '%s\n' "$out" | sed -n 's/.*issuer: //p' | head -1)"
        if [ -z "$issuer" ]; then
          reason="无法读取 ${host} 的证书 issuer"
        else
          case "$(printf '%s' "$issuer" | tr '[:upper:]' '[:lower:]')" in
            *charles*|*mitm*|*fiddler*|*zscaler*|*clash*|*surge*|*proxy*)
              reason="${host} 的证书 issuer 疑似代理/MITM: ${issuer}"
              ;;
            *)
              ui_note 'TLS 预检通过：%s via %s issuer=%s' "$host" "$CLAUDE_PROXY_URL" "$issuer"
              return 0
              ;;
          esac
        fi
      fi
    else
      reason="无法连接或验证 ${host} 的证书"
    fi

    if [ "$attempt" -lt "$TLS_CHECK_RETRIES" ]; then
      ui_warn 'TLS 预检重试：%s 第 %s/%s 次失败：%s' "$host" "$attempt" "$TLS_CHECK_RETRIES" "$reason"
      sleep 1
    fi
    attempt=$((attempt + 1))
  done

  if [ "$mode" = "warn" ]; then
    ui_warn 'TLS 预检警告：%s 经过 %s 次尝试仍未稳定通过：%s' "$host" "$TLS_CHECK_RETRIES" "$reason"
    if [ "$UI_ENABLED" -eq 1 ]; then
      out="$(printf '%s\n' "$out" | grep -Ei 'certificate|issuer|subject|SSL|error|CONNECT' || true)"
      [ -z "$out" ] || ui_note '%s' "$out"
    else
      printf '%s\n' "$out" | grep -Ei 'certificate|issuer|subject|SSL|error|CONNECT' >&2 || true
    fi
    return 0
  fi

  printf 'TLS 预检失败：%s 经过 %s 次尝试仍未通过：%s\n' "$host" "$TLS_CHECK_RETRIES" "$reason" >&2
  printf '%s\n' "$out" | grep -Ei 'certificate|issuer|subject|SSL|error|CONNECT' >&2 || true
  exit 7
}

check_anthropic_ipv6_blocked() {
  local out remote_ip

  if out="$(clean_env curl -6 -sS -o /dev/null -w 'remote_ip=%{remote_ip} http=%{http_code}' --connect-timeout 3 --max-time 6 https://api.anthropic.com/ 2>/dev/null)"; then
    remote_ip="$(printf '%s\n' "$out" | sed -n 's/.*remote_ip=\([^ ]*\).*/\1/p')"
    case "$remote_ip" in
      ::ffff:198.18.*|::ffff:198.19.*|198.18.*|198.19.*)
        ui_note 'IPv6 预检通过：api.anthropic.com 仅返回 Mihomo/Clash fake-ip 映射 %s，不是公网 IPv6 出口。' "$remote_ip"
        return 0
        ;;
      ::ffff:*)
        ui_note 'IPv6 预检通过：api.anthropic.com 返回 IPv4-mapped 地址 %s，不是公网 IPv6 出口。' "$remote_ip"
        return 0
        ;;
    esac
    printf 'IPv6 预检失败：api.anthropic.com 仍可通过 IPv6 访问，已拒绝启动。\n' >&2
    printf '当前结果：%s\n' "$out" >&2
    printf '请先关闭 Clash/Mihomo IPv6，确保 Claude 官方链路压在 IPv4。\n' >&2
    exit 8
  fi

  ui_note 'IPv6 预检通过：api.anthropic.com IPv6 不可达，Claude 官方链路将压在 IPv4。'
}

rotate_guard_log_if_needed() {
  local size lock archive

  case "$LOG_MAX_BYTES" in
    ''|*[!0-9]*) return 0 ;;
  esac
  [ "$LOG_MAX_BYTES" -gt 0 ] || return 0
  [ -f "$LOG_FILE" ] || return 0

  size="$(wc -c <"$LOG_FILE" 2>/dev/null | tr -d '[:space:]')" || return 0
  [ "${size:-0}" -gt "$LOG_MAX_BYTES" ] || return 0

  lock="${LOG_FILE}.rotate.lock"
  if ! mkdir "$lock" 2>/dev/null; then
    return 0
  fi

  size="$(wc -c <"$LOG_FILE" 2>/dev/null | tr -d '[:space:]')" || {
    rmdir "$lock" 2>/dev/null || true
    return 0
  }
  if [ "${size:-0}" -gt "$LOG_MAX_BYTES" ]; then
    archive="${LOG_FILE}.1"
    mv -f "$LOG_FILE" "$archive" 2>/dev/null || true
  fi
  rmdir "$lock" 2>/dev/null || true
}

log_guard() {
  local level="$1"
  local message="$2"
  local dir timestamp

  timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')"
  dir="$(dirname "$LOG_FILE")"
  mkdir -p "$dir"
  rotate_guard_log_if_needed
  printf '%s [%s] %s\n' "$timestamp" "$level" "$message" >>"$LOG_FILE"
}

notify_guard() {
  local message="$1"

  if [ "$NOTIFY_ENABLED" != "1" ] || ! command -v osascript >/dev/null 2>&1; then
    return 0
  fi

  osascript -e "display notification \"${message//\"/\\\"}\" with title \"Claude Guardian\"" >/dev/null 2>&1 || true
}

resolve_notification_setting() {
  local config_valid="${1:-0}"
  local configured="0"

  if [ "$NOTIFY_OVERRIDE_SET" -eq 1 ]; then
    configured="$NOTIFY_ENABLED"
  elif [ "$config_valid" -eq 1 ]; then
    if jq -e 'has("notify") and (.notify | type != "boolean")' "$CONFIG_FILE" >/dev/null; then
      printf '配置中的 notify 必须是 true 或 false。\n' >&2
      return 2
    fi
    configured="$(jq -r 'if .notify == true then "1" else "0" end' "$CONFIG_FILE")"
  fi

  case "$configured" in
    0|1) NOTIFY_ENABLED="$configured" ;;
    *)
      printf 'CLAUDE_GUARD_NOTIFY 只能是 0 或 1，当前是: %s\n' "$configured" >&2
      return 2
      ;;
  esac
}

load_visibility_config() {
  VISIBILITY_CONFIG_STATE="missing"
  VISIBILITY_CLAUDE_BIN=""
  VISIBILITY_PROFILE="$HOME/.claude-official"

  if [ -f "$CONFIG_FILE" ]; then
    if jq -e 'type == "object"' "$CONFIG_FILE" >/dev/null 2>&1; then
      VISIBILITY_CONFIG_STATE="valid"
      VISIBILITY_CLAUDE_BIN="$(jq -r '.command // empty' "$CONFIG_FILE")"
      VISIBILITY_PROFILE="$(jq -r '.config_dir // empty' "$CONFIG_FILE")"
      [ -n "$VISIBILITY_PROFILE" ] || VISIBILITY_PROFILE="$HOME/.claude-official"
      if [ -z "$VISIBILITY_CLAUDE_BIN" ]; then
        VISIBILITY_CONFIG_STATE="incomplete"
      fi
      resolve_notification_setting 1
    else
      VISIBILITY_CONFIG_STATE="invalid"
      resolve_notification_setting 0
    fi
  else
    resolve_notification_setting 0
  fi
}

visibility_display_path() {
  local path="$1"

  case "$path" in
    "$HOME") printf '~\n' ;;
    "$HOME"/*) printf '~%s\n' "${path#"$HOME"}" ;;
    *) printf '%s\n' "$path" ;;
  esac
}

visibility_log_stream() {
  if [ -f "${LOG_FILE}.1" ]; then
    cat "${LOG_FILE}.1" 2>/dev/null || true
  fi
  if [ -f "$LOG_FILE" ]; then
    cat "$LOG_FILE" 2>/dev/null || true
  fi
  return 0
}

visibility_process_rows() {
  local claude_bin="$1"
  local snapshot

  [ -n "$claude_bin" ] || return 0
  snapshot="$(ps -axo pid=,ppid=,pgid=,args= 2>/dev/null || true)"
  printf '%s\n' "$snapshot" | awk -v bin="$claude_bin" '
    {
      count += 1
      pid[count] = $1
      ppid[count] = $2
      pgid[count] = $3
      line[count] = $0
    }
    END {
      for (i = 1; i <= count; i += 1) {
        if (index(line[i], bin) > 0) {
          claude[pid[i]] = 1
        }
      }
      for (i = 1; i <= count; i += 1) {
        if (claude[ppid[i]] && line[i] ~ / bash .*claude-guard( |$)/) {
          watchdog[ppid[i]] += 1
        }
      }
      for (i = 1; i <= count; i += 1) {
        if (claude[pid[i]]) {
          printf "%s\t%s\t%s\n", pid[i], pgid[i], (watchdog[pid[i]] > 0 ? 1 : 0)
        }
      }
    }
  ' | sort -n
}

visibility_state_for_pid() {
  local target_pid="$1"
  local line state timestamp

  line="$(visibility_log_stream | awk -v pid="$target_pid" '
    $0 ~ ("pid=" pid "([^0-9]|$)") { last = $0 }
    END { print last }
  ')"
  timestamp="-"
  state="unknown"
  if [ -n "$line" ]; then
    timestamp="${line%% *}"
    case "$line" in
      *"would resume Claude"*|*"route healthy"*) state="healthy" ;;
      *"resume candidate"*) state="recovering" ;;
      *"would pause Claude"*) state="would-pause" ;;
      *"exit ip unavailable"*|*"exit ip not allowed"*|*"api path unhealthy"*|*"wake gap detected"*) state="degraded" ;;
      *"watchdog exited"*) state="exited" ;;
      *"watchdog started"*) state="observing" ;;
    esac
  fi
  printf '%s|%s\n' "$state" "$timestamp"
}

visibility_session_rows() {
  local base_rows target_pid pgid watchdog state_info state timestamp

  base_rows="$(visibility_process_rows "$VISIBILITY_CLAUDE_BIN")"
  [ -n "$base_rows" ] || return 0
  while IFS=$'\t' read -r target_pid pgid watchdog; do
    [ -n "$target_pid" ] || continue
    state_info="$(visibility_state_for_pid "$target_pid")"
    state="${state_info%%|*}"
    timestamp="${state_info#*|}"
    printf '%s\t%s\t%s\t%s\t%s\n' \
      "$target_pid" "$pgid" "$watchdog" "$state" "$timestamp"
  done <<EOF
$base_rows
EOF
}

visibility_last_event() {
  local pattern="$1"

  visibility_log_stream | awk -v pattern="$pattern" '
    $0 ~ pattern { last = $0 }
    END { print last }
  '
}

visibility_log_bytes() {
  local bytes=0 size

  if [ -f "${LOG_FILE}.1" ]; then
    size="$(wc -c 2>/dev/null <"${LOG_FILE}.1" | tr -d '[:space:]')" || size=0
    bytes=$((bytes + ${size:-0}))
  fi
  if [ -f "$LOG_FILE" ]; then
    size="$(wc -c 2>/dev/null <"$LOG_FILE" | tr -d '[:space:]')" || size=0
    bytes=$((bytes + ${size:-0}))
  fi
  printf '%s\n' "$bytes"
}

visibility_sessions_json() {
  local rows="$1"

  printf '%s\n' "$rows" | jq -R -s '
    split("\n")
    | map(select(length > 0) | split("\t"))
    | map({
        pid: (.[0] | tonumber),
        pgid: (.[1] | tonumber),
        watchdog: (.[2] == "1"),
        state: .[3],
        last_event_at: .[4]
      })
  '
}

print_status_report() {
  local rows sessions_json active_count watchdog_count mode mode_label notify_label health health_label
  local profile_display log_display log_bytes last_incident last_recovery

  load_visibility_config
  rows="$(visibility_session_rows)"
  active_count="$(printf '%s\n' "$rows" | awk 'NF { count += 1 } END { print count + 0 }')"
  watchdog_count="$(printf '%s\n' "$rows" | awk -F '\t' 'NF { count += $3 } END { print count + 0 }')"
  mode="action-refused"
  mode_label="action requested (refused at launch)"
  if [ "$WATCHDOG_DRY_RUN" = "1" ]; then
    mode="dry-run"
    mode_label="dry-run observation"
  fi
  notify_label="disabled"
  [ "$NOTIFY_ENABLED" = "1" ] && notify_label="enabled"
  health="idle"
  health_label="IDLE"
  if [ "$active_count" -gt 0 ]; then
    health="ok"
    health_label="OK"
    if [ "$watchdog_count" -lt "$active_count" ] || \
       printf '%s\n' "$rows" | awk -F '\t' '$4 ~ /^(degraded|would-pause|recovering|unknown|exited)$/ { found = 1 } END { exit !found }'; then
      health="warn"
      health_label="WARN"
    fi
  fi
  profile_display="$(visibility_display_path "$VISIBILITY_PROFILE")"
  log_display="$(visibility_display_path "$LOG_FILE")"
  log_bytes="$(visibility_log_bytes)"
  last_incident="$(visibility_last_event 'would pause Claude|exit ip unavailable|exit ip not allowed|api path unhealthy|wake gap detected')"
  last_recovery="$(visibility_last_event 'would resume Claude|route healthy')"

  if [ "$STATUS_JSON" -eq 1 ]; then
    sessions_json="$(visibility_sessions_json "$rows")"
    jq -n \
      --arg version "$VERSION" \
      --arg mode "$mode" \
      --arg health "$health" \
      --arg config_state "$VISIBILITY_CONFIG_STATE" \
      --arg proxy "$CLAUDE_PROXY_URL" \
      --arg profile "$profile_display" \
      --arg log_file "$log_display" \
      --arg last_incident "$last_incident" \
      --arg last_recovery "$last_recovery" \
      --argjson notifications "$([ "$NOTIFY_ENABLED" = "1" ] && printf true || printf false)" \
      --argjson active_sessions "$active_count" \
      --argjson active_watchdogs "$watchdog_count" \
      --argjson log_bytes "$log_bytes" \
      --argjson sessions "$sessions_json" \
      '{
        version: $version,
        health: $health,
        mode: $mode,
        notifications: $notifications,
        config_state: $config_state,
        proxy: $proxy,
        profile: $profile,
        active_sessions: $active_sessions,
        active_watchdogs: $active_watchdogs,
        log_file: $log_file,
        log_bytes: $log_bytes,
        last_incident: (if $last_incident == "" then null else $last_incident end),
        last_recovery: (if $last_recovery == "" then null else $last_recovery end),
        sessions: $sessions
      }'
    return 0
  fi

  printf 'Claude Guard Status\n'
  printf '  version: %s\n' "$VERSION"
  printf '  health: %s\n' "$health_label"
  printf '  mode: %s\n' "$mode_label"
  printf '  notifications: %s\n' "$notify_label"
  printf '  config: %s\n' "$VISIBILITY_CONFIG_STATE"
  printf '  profile: %s\n' "$profile_display"
  printf '  proxy: %s\n' "$CLAUDE_PROXY_URL"
  printf '  active Claude sessions: %s\n' "$active_count"
  printf '  active watchdogs: %s\n' "$watchdog_count"
  printf '  log: %s (%s bytes including archive)\n' "$log_display" "$log_bytes"
  printf 'Sessions:\n'
  if [ -z "$rows" ]; then
    printf '  none\n'
  else
    while IFS=$'\t' read -r target_pid pgid watchdog state timestamp; do
      [ -n "$target_pid" ] || continue
      if [ "$watchdog" = "1" ]; then
        watchdog="yes"
      else
        watchdog="no"
      fi
      printf '  pid=%s pgid=%s watchdog=%s state=%s last=%s\n' \
        "$target_pid" "$pgid" "$watchdog" "$state" "$timestamp"
    done <<EOF
$rows
EOF
  fi
  printf 'History:\n'
  printf '  last incident: %s\n' "${last_incident:-none}"
  printf '  last recovery: %s\n' "${last_recovery:-none}"
}

visibility_redact_stream() {
  awk -v home="$HOME" '
    function replace_literal(value, needle, replacement, result, position) {
      if (needle == "") return value
      result = ""
      while ((position = index(value, needle)) > 0) {
        result = result substr(value, 1, position - 1) replacement
        value = substr(value, position + length(needle))
      }
      return result value
    }
    {
      line = replace_literal($0, home, "~")
      gsub(/[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?/, "x.x.x.x", line)
      gsub(/[Tt][Oo][Kk][Ee][Nn]=[^ ]+/, "token=[REDACTED]", line)
      gsub(/[Ss][Ee][Cc][Rr][Ee][Tt]=[^ ]+/, "secret=[REDACTED]", line)
      gsub(/[Aa][Uu][Tt][Hh]=[^ ]+/, "auth=[REDACTED]", line)
      gsub(/[Kk][Ee][Yy]=[^ ]+/, "key=[REDACTED]", line)
      gsub(/path=\/[^ ]+/, "path=[REDACTED]", line)
      gsub(/profile=\/[^ ]+/, "profile=[REDACTED]", line)
      gsub(/command=\/[^ ]+/, "command=[REDACTED]", line)
      print line
    }
  '
}

print_diagnostic_report() {
  local rows active_count watchdog_count allowed_count mode_label

  load_visibility_config
  rows="$(visibility_session_rows)"
  active_count="$(printf '%s\n' "$rows" | awk 'NF { count += 1 } END { print count + 0 }')"
  watchdog_count="$(printf '%s\n' "$rows" | awk -F '\t' 'NF { count += $3 } END { print count + 0 }')"
  allowed_count=0
  if [ "$VISIBILITY_CONFIG_STATE" = "valid" ]; then
    allowed_count="$(jq -r '[(.allowed_ips // [])[], (.allowed_cidrs // [])[]] | length' "$CONFIG_FILE")"
  fi
  mode_label="action-refused"
  [ "$WATCHDOG_DRY_RUN" = "1" ] && mode_label="dry-run"

  {
    printf 'Claude Guard Diagnostic (redacted)\n'
    printf 'generated_at=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
    printf 'version=%s\n' "$VERSION"
    printf 'platform=%s\n' "$(uname -srm 2>/dev/null || printf unknown)"
    printf 'mode=%s\n' "$mode_label"
    printf 'notifications=%s\n' "$NOTIFY_ENABLED"
    printf 'config_state=%s\n' "$VISIBILITY_CONFIG_STATE"
    printf 'profile=%s\n' "$VISIBILITY_PROFILE"
    printf 'proxy=%s\n' "$CLAUDE_PROXY_URL"
    printf 'allowed_entries=%s\n' "$allowed_count"
    printf 'active_sessions=%s\n' "$active_count"
    printf 'active_watchdogs=%s\n' "$watchdog_count"
    printf 'log_file=%s\n' "${LOG_FILE##*/}"
    printf 'log_bytes=%s\n' "$(visibility_log_bytes)"
    printf 'event_counts:\n'
    visibility_log_stream | awk '
      /watchdog started/ { started += 1 }
      /exit ip unavailable|exit ip unknown/ { unavailable += 1 }
      /exit ip not allowed/ { not_allowed += 1 }
      /api path unhealthy/ { api += 1 }
      /would pause Claude/ { paused += 1 }
      /would resume Claude/ { resumed += 1 }
      /watchdog exited|watchdog exiting because Claude/ { exited += 1 }
      END {
        printf "  watchdog_started=%d\n", started
        printf "  ip_unavailable=%d\n", unavailable
        printf "  ip_not_allowed=%d\n", not_allowed
        printf "  api_unhealthy=%d\n", api
        printf "  would_pause=%d\n", paused
        printf "  would_resume=%d\n", resumed
        printf "  watchdog_exited=%d\n", exited
      }
    '
    printf 'recent_events:\n'
    visibility_log_stream | awk '
      /watchdog|DRY RUN|api path unhealthy|exit ip|wake gap|route healthy|resume candidate/ { print }
    ' | tail -n 20
  } | visibility_redact_stream
}

current_timezone() {
  node -e 'console.log(Intl.DateTimeFormat().resolvedOptions().timeZone || "")' 2>/dev/null || true
}

redact_value() {
  local value="$1"

  if [ "${#value}" -le 12 ]; then
    printf '%s\n' "$value"
  else
    printf '%s...\n' "${value:0:8}"
  fi
}

client_scan_files() {
  local file="$1"
  local dir package_root="" scope_dir

  file="$(resolve_path "$file")"
  printf '%s\n' "$file"

  dir="$file"
  while [ "$dir" != "/" ] && [ -n "$dir" ]; do
    if [ "$(basename "$dir")" = "claude-code" ] && [ "$(basename "$(dirname "$dir")")" = "@anthropic-ai" ]; then
      package_root="$dir"
      break
    fi
    dir="$(dirname "$dir")"
  done

  if [ -n "$package_root" ]; then
    scope_dir="$(dirname "$package_root")"
    find "$scope_dir" -maxdepth 3 -type f \( -name 'claude' -o -name 'claude.exe' -o -name 'cli.js' -o -name 'cli-wrapper.cjs' \) 2>/dev/null
  fi
}

find_client_fingerprint_marker() {
  local file="$1"
  local scan_file seen=":"

  while IFS= read -r scan_file; do
    [ -n "$scan_file" ] || continue
    case "$seen" in
      *:"$scan_file":*) continue ;;
    esac
    seen="${seen}${scan_file}:"
    [ -r "$scan_file" ] || continue
    if LC_ALL=C grep -aEq 'Asia/Shanghai|Asia/Urumqi' "$scan_file" &&
      LC_ALL=C grep -aEq '\\u02BC|\\u02B9|Today.*date is|date is.*replaceAll|date is.*replace\(' "$scan_file"; then
      printf '%s\n' "$scan_file"
      return 0
    fi
  done <<EOF
$(client_scan_files "$file")
EOF

  return 1
}

check_legacy_profile_contamination() {
  local profile="$HOME/.claude/settings.json"
  local marker_re='ANTHROPIC_BASE_URL|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY|apiKeyHelper|127\.0\.0\.1:15721|PROXY_MANAGED|CLAUDE_CODE_USE_GATEWAY'
  local markers

  [ "$LEGACY_PROFILE_MODE" = "off" ] && return 0
  [ -f "$profile" ] || return 0
  if ! grep -Eiq "$marker_re" "$profile"; then
    return 0
  fi

  if [ "$LEGACY_PROFILE_MODE" = "strict" ]; then
    printf '旧默认 Claude profile 含有反代/凭据注入残留，strict 模式已拒绝启动: %s\n' "$profile" >&2
    markers="$(grep -Eio "$marker_re" "$profile" | sort -u || true)"
    printf '%s\n' "$markers" | sed 's/^/  - /' >&2
    exit 11
  fi

  ui_warn '旧默认 Claude profile 命中反代/凭据注入残留，但官方入口使用 CLAUDE_CONFIG_DIR=%s，按 warn 模式继续。' "$OFFICIAL_CONFIG_DIR"
  log_guard "WARN" "legacy profile contains proxy/auth markers path=${profile}; official profile remains isolated"
}

check_client_fingerprint_tripwire() {
  local claude_bin="$1"
  local resolved_bin tz active_reasons marker_found=0 current_base_url marker_file

  case "$FINGERPRINT_MODE" in
    off|warn|fail-active|strict) ;;
    *)
      printf 'CLAUDE_GUARD_FINGERPRINT_MODE 只能是 off、warn、fail-active 或 strict，当前是: %s\n' "$FINGERPRINT_MODE" >&2
      exit 10
      ;;
  esac

  [ "$FINGERPRINT_MODE" = "off" ] && return 0

  resolved_bin="$(resolve_path "$claude_bin")"
  marker_file="$(find_client_fingerprint_marker "$resolved_bin" || true)"
  if [ -n "$marker_file" ]; then
    marker_found=1
  fi

  if [ "$marker_found" -ne 1 ]; then
    ui_note '客户端指纹预检通过：未发现已知 date/time watermark 逻辑。'
    return 0
  fi

  tz="$(current_timezone)"
  current_base_url="${ANTHROPIC_BASE_URL:-}"
  active_reasons=""

  if [ -n "$current_base_url" ]; then
    active_reasons="${active_reasons} ANTHROPIC_BASE_URL=$(redact_value "$current_base_url")"
  fi
  case "$tz" in
    Asia/Shanghai|Asia/Urumqi)
      active_reasons="${active_reasons} timezone=${tz}"
      ;;
  esac

  if [ "$FINGERPRINT_MODE" = "strict" ]; then
    printf '客户端指纹预检失败：%s 含有已知 date/time watermark 逻辑，strict 模式拒绝启动。\n' "$marker_file" >&2
    exit 10
  fi

  if [ -n "$active_reasons" ]; then
    if [ "$FINGERPRINT_MODE" = "fail-active" ]; then
      printf '客户端指纹预检失败：%s 含有已知 date/time watermark 逻辑，且存在激活条件:%s\n' "$marker_file" "$active_reasons" >&2
      printf '请清理 ANTHROPIC_BASE_URL 或改用干净 timezone/profile 后再启动官方 Claude。\n' >&2
      exit 10
    fi
    ui_warn '客户端指纹预检警告：%s 含有已知 date/time watermark 逻辑，且存在激活条件:%s' "$marker_file" "$active_reasons"
    log_guard "WARN" "watermark-capable client with active conditions:${active_reasons}"
    return 0
  fi

  ui_warn '客户端指纹预检警告：%s 含有已知 date/time watermark 逻辑，但当前官方环境未发现激活条件，继续。' "$marker_file"
  log_guard "WARN" "watermark-capable client detected without active conditions path=${marker_file}"
}

is_ipv4() {
  local ip="$1"
  local a b c d

  IFS=. read -r a b c d <<EOF
$ip
EOF
  for part in "$a" "$b" "$c" "$d"; do
    case "$part" in
      ''|*[!0-9]*) return 1 ;;
    esac
    if [ "$part" -lt 0 ] || [ "$part" -gt 255 ]; then
      return 1
    fi
  done
  [ -n "$d" ] || return 1
  return 0
}

ipv4_to_int() {
  local ip="$1"
  local a b c d

  IFS=. read -r a b c d <<EOF
$ip
EOF
  printf '%u\n' "$(( (a << 24) + (b << 16) + (c << 8) + d ))"
}

cidr_contains_ip() {
  local ip="$1"
  local cidr="$2"
  local base prefix ip_num base_num mask

  base="${cidr%/*}"
  prefix="${cidr#*/}"
  is_ipv4 "$ip" || return 1
  is_ipv4 "$base" || return 1
  case "$prefix" in
    ''|*[!0-9]*) return 1 ;;
  esac
  if [ "$prefix" -lt 0 ] || [ "$prefix" -gt 32 ]; then
    return 1
  fi

  ip_num="$(ipv4_to_int "$ip")"
  base_num="$(ipv4_to_int "$base")"
  if [ "$prefix" -eq 0 ]; then
    mask=0
  else
    mask="$(( (0xffffffff << (32 - prefix)) & 0xffffffff ))"
  fi
  [ "$((ip_num & mask))" -eq "$((base_num & mask))" ]
}

get_current_ip_once() {
  local url ip

  for url in $IP_CHECK_URLS; do
    ip="$(official_net_env curl -4 -x "$CLAUDE_PROXY_URL" -fsS --connect-timeout 5 --max-time 10 "$url" 2>/dev/null | tr -d '[:space:]' || true)"
    if is_ipv4 "$ip"; then
      printf '%s\n' "$ip"
      return 0
    fi
  done
  return 1
}

is_allowed_ip() {
  local ip="$1"
  local cidr

  if jq -e --arg ip "$ip" '.allowed_ips // [] | index($ip)' "$CONFIG_FILE" >/dev/null; then
    return 0
  fi

  while IFS= read -r cidr; do
    [ -n "$cidr" ] || continue
    if cidr_contains_ip "$ip" "$cidr"; then
      return 0
    fi
  done <<EOF
$(jq -r '[(.allowed_ips // [])[], (.allowed_cidrs // [])[]] | .[] | select(type == "string" and contains("/"))' "$CONFIG_FILE")
EOF

  return 1
}

check_api_path_once() {
  local out issuer

  if ! out="$(official_net_env curl -4 -x "$CLAUDE_PROXY_URL" -vI --connect-timeout 8 --max-time 15 https://api.anthropic.com/ 2>&1 >/dev/null)"; then
    return 1
  fi
  printf '%s\n' "$out" | grep -Eq "CONNECT api.anthropic.com:443|CONNECT tunnel established|CONNECT phase completed" || return 1
  printf '%s\n' "$out" | grep -q 'SSL certificate verify ok' || return 1
  issuer="$(printf '%s\n' "$out" | sed -n 's/.*issuer: //p' | head -1)"
  [ -n "$issuer" ] || return 1
  case "$(printf '%s' "$issuer" | tr '[:upper:]' '[:lower:]')" in
    *charles*|*mitm*|*fiddler*|*zscaler*|*clash*|*surge*|*proxy*) return 1 ;;
  esac
  return 0
}

watchdog_log() {
  local target_pid="$1"
  local level="$2"
  local message="$3"

  log_guard "$level" "watchdog pid=${target_pid} ${message}"
}

watchdog_signal_exit() {
  local target_pid="$1"
  local signal="$2"

  trap - HUP INT TERM
  watchdog_log "$target_pid" "INFO" "watchdog exited reason=signal-${signal}"
  exit 0
}

pause_target() {
  local target_pid="$1"
  local reason="$2"

  if [ "$WATCHDOG_DRY_RUN" = "1" ]; then
    log_guard "WARN" "DRY RUN: would pause Claude pid=${target_pid}; reason=${reason}"
    if [ "$DRY_RUN_STDERR" = "1" ]; then
      printf '\nClaude Guardian dry-run: would pause Claude because %s\n' "$reason" >&2
    fi
    notify_guard "Dry-run: would pause Claude pid ${target_pid} because ${reason}"
    return 0
  fi

  printf 'Claude Guardian action mode is disabled in v%s because this launcher preserves Claude as the foreground TUI process and cannot safely pause the whole process group yet. Keep CLAUDE_GUARD_WATCHDOG_DRY_RUN=1.\n' "$VERSION" >&2
  log_guard "ERROR" "action mode refused; unsafe to pause only pid=${target_pid}; reason=${reason}"
  return 1
}

resume_target() {
  local target_pid="$1"
  local reason="$2"

  if [ "$WATCHDOG_DRY_RUN" = "1" ]; then
    log_guard "INFO" "DRY RUN: would resume Claude pid=${target_pid}; reason=${reason}"
    if [ "$DRY_RUN_STDERR" = "1" ]; then
      printf '\nClaude Guardian dry-run: would resume Claude because %s\n' "$reason" >&2
    fi
    notify_guard "Dry-run: would resume Claude pid ${target_pid} because ${reason}"
    return 0
  fi

  printf 'Claude Guardian action mode is disabled in v%s; cannot resume pid=%s safely.\n' "$VERSION" "$target_pid" >&2
  log_guard "ERROR" "action mode refused; unsafe to resume only pid=${target_pid}; reason=${reason}"
  return 1
}

run_watchdog() {
  local target_pid="$1"
  local state="healthy"
  local current_ip=""
  local ip_status="unavailable"
  local ip_ok=0
  local api_ok=1
  local ip_bad_count=0
  local api_bad_count=0
  local resume_good_count=0
  local recovery_failure_count=0
  local paused_since=0
  local kill_notice_logged=0
  local last_tick now next_ip_check next_api_check next_recovery_check reason previous_count

  last_tick="$(date +%s)"
  next_ip_check="$last_tick"
  next_api_check="$last_tick"
  next_recovery_check="$last_tick"
  log_guard "INFO" "watchdog started pid=${target_pid} proxy=${CLAUDE_PROXY_URL} dry_run=${WATCHDOG_DRY_RUN}"
  trap 'watchdog_signal_exit "$target_pid" HUP' HUP
  trap 'watchdog_signal_exit "$target_pid" INT' INT
  trap 'watchdog_signal_exit "$target_pid" TERM' TERM

  while kill -0 "$target_pid" 2>/dev/null; do
    now="$(date +%s)"

    if [ $((now - last_tick)) -gt "$WAKE_GAP_SECONDS" ]; then
      watchdog_log "$target_pid" "WARN" "wake gap detected gap_seconds=$((now - last_tick)) state=${state}"
      resume_good_count=0
      if [ "$state" = "paused" ] || [ "$state" = "resume-pending" ]; then
        state="paused"
        next_recovery_check="$now"
      else
        state="degraded"
        next_ip_check="$now"
        next_api_check="$now"
      fi
    fi
    last_tick="$now"

    if [ "$state" = "paused" ] || [ "$state" = "resume-pending" ]; then
      if [ "$now" -ge "$next_recovery_check" ]; then
        current_ip="$(get_current_ip_once || true)"
        if [ -z "$current_ip" ]; then
          ip_status="unavailable"
          ip_ok=0
        elif is_allowed_ip "$current_ip"; then
          ip_status="allowed"
          ip_ok=1
        else
          ip_status="not-allowed"
          ip_ok=0
        fi

        if check_api_path_once; then
          api_ok=1
        else
          api_ok=0
        fi

        if [ "$ip_ok" -eq 1 ] && [ "$api_ok" -eq 1 ]; then
          state="resume-pending"
          recovery_failure_count=0
          resume_good_count=$((resume_good_count + 1))
          watchdog_log "$target_pid" "INFO" "resume candidate count=${resume_good_count}/${RESUME_GOOD_THRESHOLD} ip=${current_ip}"
          if [ "$resume_good_count" -ge "$RESUME_GOOD_THRESHOLD" ]; then
            reason="route healthy again ip=${current_ip}"
            resume_target "$target_pid" "$reason" || true
            state="healthy"
            ip_bad_count=0
            api_bad_count=0
            resume_good_count=0
            paused_since=0
            kill_notice_logged=0
            next_ip_check=$((now + IP_INTERVAL))
            next_api_check=$((now + API_INTERVAL))
          fi
        else
          state="paused"
          resume_good_count=0
          recovery_failure_count=$((recovery_failure_count + 1))
          if [ "$recovery_failure_count" -eq 1 ] || [ $((recovery_failure_count % 10)) -eq 0 ]; then
            watchdog_log "$target_pid" "WARN" \
              "recovery check unhealthy attempt=${recovery_failure_count} ip_status=${ip_status} api_ok=${api_ok}"
          fi
        fi
        next_recovery_check=$(($(date +%s) + RECOVERY_INTERVAL))
      fi

      if [ -n "$KILL_AFTER_PAUSE_SECONDS" ] && [ "$paused_since" -gt 0 ] && \
         [ $((now - paused_since)) -ge "$KILL_AFTER_PAUSE_SECONDS" ] && \
         [ "$kill_notice_logged" -eq 0 ]; then
        watchdog_log "$target_pid" "ERROR" \
          "kill-after-pause reached but automatic kill is not implemented in v${VERSION}"
        kill_notice_logged=1
      fi

      sleep "$WATCHDOG_TICK_SECONDS"
      continue
    fi

    if [ "$now" -ge "$next_ip_check" ]; then
      current_ip="$(get_current_ip_once || true)"
      if [ -z "$current_ip" ]; then
        ip_status="unavailable"
        ip_ok=0
        previous_count="$ip_bad_count"
        if [ "$ip_bad_count" -lt "$IP_BAD_THRESHOLD" ]; then
          ip_bad_count=$((ip_bad_count + 1))
        fi
        if [ "$ip_bad_count" -ne "$previous_count" ]; then
          watchdog_log "$target_pid" "WARN" \
            "exit ip unavailable count=${ip_bad_count}/${IP_BAD_THRESHOLD} state=${state}"
        fi
      elif is_allowed_ip "$current_ip"; then
        ip_status="allowed"
        ip_ok=1
        ip_bad_count=0
      else
        ip_status="not-allowed"
        ip_ok=0
        previous_count="$ip_bad_count"
        if [ "$ip_bad_count" -lt "$IP_BAD_THRESHOLD" ]; then
          ip_bad_count=$((ip_bad_count + 1))
        fi
        if [ "$ip_bad_count" -ne "$previous_count" ]; then
          watchdog_log "$target_pid" "WARN" \
            "exit ip not allowed ip=${current_ip} count=${ip_bad_count}/${IP_BAD_THRESHOLD} state=${state}"
        fi
      fi
      next_ip_check=$((now + IP_INTERVAL))
    fi

    if [ "$now" -ge "$next_api_check" ]; then
      if check_api_path_once; then
        api_ok=1
        api_bad_count=0
      else
        api_ok=0
        previous_count="$api_bad_count"
        if [ "$api_bad_count" -lt "$API_BAD_THRESHOLD" ]; then
          api_bad_count=$((api_bad_count + 1))
        fi
        if [ "$api_bad_count" -ne "$previous_count" ]; then
          watchdog_log "$target_pid" "WARN" \
            "api path unhealthy count=${api_bad_count}/${API_BAD_THRESHOLD} state=${state}"
        fi
      fi
      next_api_check=$((now + API_INTERVAL))
    fi

    if [ "$ip_bad_count" -ge "$IP_BAD_THRESHOLD" ]; then
      state="paused"
      paused_since="$now"
      if [ "$ip_status" = "unavailable" ]; then
        reason="exit IP check unavailable"
      else
        reason="exit IP not allowed ip=${current_ip}"
      fi
      pause_target "$target_pid" "$reason" || true
      next_recovery_check=$(($(date +%s) + RECOVERY_INTERVAL))
    elif [ "$api_bad_count" -ge "$API_BAD_THRESHOLD" ]; then
      state="paused"
      paused_since="$now"
      reason="api path unhealthy"
      pause_target "$target_pid" "$reason" || true
      next_recovery_check=$(($(date +%s) + RECOVERY_INTERVAL))
    elif [ "$ip_ok" -eq 1 ] && [ "$api_ok" -eq 1 ]; then
      if [ "$state" != "healthy" ]; then
        watchdog_log "$target_pid" "INFO" "route healthy ip=${current_ip:-unknown}"
      fi
      state="healthy"
    else
      state="degraded"
    fi

    sleep "$WATCHDOG_TICK_SECONDS"
  done

  trap - HUP INT TERM
  watchdog_log "$target_pid" "INFO" "watchdog exited reason=target-gone"
}

start_watchdog_sidecar() {
  if [ "$WATCHDOG_ENABLED" != "1" ]; then
    return 0
  fi

  if [ "$WATCHDOG_DRY_RUN" != "1" ]; then
    printf 'Claude Guardian action mode is not enabled in v%s. This version defaults to dry-run observation so it cannot accidentally pause only the main PID.\n' "$VERSION" >&2
    printf 'Please use CLAUDE_GUARD_WATCHDOG_DRY_RUN=1, observe logs, and enable real pause/resume after process-group runner support lands.\n' >&2
    exit 9
  fi

  run_watchdog "$$" &
}

case "$COMMAND_MODE" in
  status)
    print_status_report
    exit 0
    ;;
  diagnose)
    print_diagnostic_report
    exit 0
    ;;
esac

if [ ! -f "$CONFIG_FILE" ]; then
  printf 'IP 安全配置不存在: %s\n' "$CONFIG_FILE" >&2
  printf '请创建配置，例如 {"command":"/path/to/claude","allowed_ips":["x.x.x.x"],"allowed_cidrs":["x.x.x.0/24"]}\n' >&2
  exit 2
fi

validate_json_file "$CONFIG_FILE" "IP 安全配置"
resolve_notification_setting 1
CLAUDE_BIN="$(jq -r '.command // empty' "$CONFIG_FILE")"
OFFICIAL_CONFIG_DIR="$(jq -r '.config_dir // empty' "$CONFIG_FILE")"
[ -n "$OFFICIAL_CONFIG_DIR" ] || OFFICIAL_CONFIG_DIR="$HOME/.claude-official"
if [ -n "$OFFICIAL_SETTINGS_OVERRIDE" ]; then
  OFFICIAL_SETTINGS="$OFFICIAL_SETTINGS_OVERRIDE"
else
  OFFICIAL_SETTINGS="$OFFICIAL_CONFIG_DIR/settings.json"
fi
if [ -z "$CLAUDE_BIN" ]; then
  printf '配置中的 command 不能为空。\n' >&2
  exit 2
fi
case "$CLAUDE_BIN" in
  /*) ;;
  *)
    printf '配置中的 command 必须是原始 Claude CLI 的绝对路径，不能写成: %s\n' "$CLAUDE_BIN" >&2
    exit 2
    ;;
esac
case "$OFFICIAL_CONFIG_DIR" in
  /*) ;;
  *)
    printf '配置中的 config_dir 必须是绝对路径: %s\n' "$OFFICIAL_CONFIG_DIR" >&2
    exit 2
    ;;
esac
if [ "$CLAUDE_BIN" = "$0" ]; then
  printf '配置中的 command 指向了 claude-guard 自己，会造成递归调用: %s\n' "$CLAUDE_BIN" >&2
  exit 2
fi
if [ ! -x "$CLAUDE_BIN" ]; then
  printf '配置中的 command 不存在或不可执行: %s\n' "$CLAUDE_BIN" >&2
  exit 2
fi

if [ ! -f "$OFFICIAL_SETTINGS" ]; then
  printf '官方旁路 settings 不存在: %s\n' "$OFFICIAL_SETTINGS" >&2
  exit 2
fi

if [ ! -f "$CA_CERT_FILE" ]; then
  printf '系统 CA bundle 不存在，已拒绝启动: %s\n' "$CA_CERT_FILE" >&2
  exit 2
fi

ui_header

ui_step_start "配置与生命周期"
check_official_settings_policy
ui_step_finish

ui_step_start "项目与参数边界"
check_project_settings_policy
check_cli_policy "$@"
check_model_policy "$@"
ui_step_finish

ui_step_start "客户端身份与指纹"
check_client_identity "$CLAUDE_BIN"
check_legacy_profile_contamination
check_client_fingerprint_tripwire "$CLAUDE_BIN"
ui_step_finish

ui_step_start "出口 IP 获取"
attempt=1
CURRENT_IP=''
while [ "$attempt" -le "$IP_CHECK_RETRIES" ]; do
  if CURRENT_IP="$(get_current_ip_once)" && [ -n "$CURRENT_IP" ]; then
    break
  fi
  if [ "$attempt" -lt "$IP_CHECK_RETRIES" ]; then
    ui_warn 'IP 检查重试：第 %s/%s 次无法从任一 IP 源获取有效 IPv4。' "$attempt" "$IP_CHECK_RETRIES"
    sleep 1
  fi
  attempt=$((attempt + 1))
done
if [ -z "$CURRENT_IP" ]; then
  printf 'IP 检查失败：无法获取当前出口 IP。\n' >&2
  exit 3
fi
ui_visual_note '出口 IP 已获取：%s' "$CURRENT_IP"
ui_step_finish

ui_step_start "Anthropic API TLS"
ui_note 'Claude Official 网络路径：%s，NO_PROXY=%s' "$CLAUDE_PROXY_URL" "$NO_PROXY_LIST"
check_tls_host api.anthropic.com
ui_step_finish

ui_step_start "Claude 登录 TLS"
check_tls_host platform.claude.com "$PLATFORM_TLS_MODE"
ui_step_finish

ui_step_start "IPv6 泄漏"
check_anthropic_ipv6_blocked
ui_step_finish

ui_step_start "出口白名单"
if is_allowed_ip "$CURRENT_IP"; then
  ui_note 'IP 检查通过：%s' "$CURRENT_IP"
  ui_step_finish
  ui_finish
  if [ "$PRECHECK_ONLY" = "1" ]; then
    if [ "$COMMAND_MODE" = "doctor" ]; then
      printf 'Doctor result: PASS (Claude was not started)\n'
      print_status_report
    elif [ "$UI_ENABLED" -eq 1 ]; then
      printf '  %sprecheck-only：未启动 Claude。%s\n' "$UI_DIM" "$UI_RESET" >&2
    else
      printf '预检完成：未启动 Claude。\n'
    fi
    exit 0
  fi
  if [ "$ASSUME_YES" != "1" ]; then
    printf '按回车继续执行 Claude Official，或按 Ctrl+C 取消。'
    read -r _
  fi
else
  printf 'IP 检查失败：%s\n' "$CURRENT_IP" >&2
  printf '正在使用未授权的网络环境。\n' >&2
  printf '允许的出口 IP/CIDR: ' >&2
  jq -r '[(.allowed_ips // [])[], (.allowed_cidrs // [])[]] | join(", ")' "$CONFIG_FILE" >&2
  printf 'v%s 不提供 unsafe 绕过；已退出，未启动 Claude Official。\n' "$VERSION" >&2
  exit 4
fi

start_watchdog_sidecar

exec env -i \
  HOME="$HOME" \
  USER="${USER:-user}" \
  PATH="$PATH" \
  TERM="${TERM:-xterm-256color}" \
  LANG="${LANG:-en_US.UTF-8}" \
  LC_ALL="${LC_ALL:-en_US.UTF-8}" \
  SHELL="${SHELL:-/bin/zsh}" \
  HTTP_PROXY="$CLAUDE_PROXY_URL" \
  HTTPS_PROXY="$CLAUDE_PROXY_URL" \
  ALL_PROXY="$CLAUDE_PROXY_URL" \
  http_proxy="$CLAUDE_PROXY_URL" \
  https_proxy="$CLAUDE_PROXY_URL" \
  all_proxy="$CLAUDE_PROXY_URL" \
  NO_PROXY="$NO_PROXY_LIST" \
  no_proxy="$NO_PROXY_LIST" \
  CLAUDE_CONFIG_DIR="$OFFICIAL_CONFIG_DIR" \
  DISABLE_TELEMETRY="1" \
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1" \
  DISABLE_UPDATES="1" \
  CLAUDE_CODE_DISABLE_AGENT_VIEW="1" \
  CLAUDE_CODE_DISABLE_BACKGROUND_TASKS="1" \
  CLAUDE_CODE_DISABLE_CRON="1" \
  CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF="1" \
  CLAUDE_DISABLE_ADOPT="1" \
  CLAUDE_CODE_DISABLE_WORKFLOWS="1" \
  CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS="0" \
  CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH="1" \
  CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS="3" \
  CLAUDE_CODE_CERT_STORE="system" \
  SSL_CERT_FILE="$CA_CERT_FILE" \
  NODE_EXTRA_CA_CERTS="$CA_CERT_FILE" \
  "$CLAUDE_BIN" \
  --setting-sources=user,project \
  --settings "$OFFICIAL_SETTINGS" \
  "$@"
