spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / plugins / security-guidance / hooks / sg-python.sh
122 lines · 5.3 KBRaw
1#!/usr/bin/env bash
2# Find a working Python 3 interpreter and exec the hook with it.
3#
4# On Windows + Git Bash, `python3` typically resolves to the Microsoft Store
5# stub at C:\Users\<user>\AppData\Local\Microsoft\WindowsApps\python3, which
6# exits 49 silently in non-TTY subprocess context (a known Microsoft Store
7# stub behavior). This shim
8# probes each candidate with `-c ""` and skips any that fails, so the Store
9# stub falls through to the real python.org install (`python` in Git Bash) or
10# the `py -3` launcher.
11#
12# Order:
13# 1. python3 — canonical on macOS/Linux; the Store stub fails the probe.
14# 2. python — python.org installs on Windows; some Linux distros (RHEL 7
15# EOL'd 2024-06) point this at Python 2, but `-c ""` succeeds
16# on Python 2 too — guard with a version check.
17# 3. py -3 — Windows Python launcher.
18#
19# Args after the shim path are passed straight through to the chosen
20# interpreter, so the hooks.json invocation is:
21# bash "${CLAUDE_PLUGIN_ROOT}/hooks/sg-python.sh" \
22# "${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py"
23set -e
24
25# Force UTF-8 for ALL Python filesystem + IO operations (PEP 540).
26# Without this, Windows Python defaults `locale.getpreferredencoding()` to
27# cp1252 — which makes `text=True` in subprocess.run / open() / json.load
28# crash the internal reader thread on any byte that's undefined in cp1252
29# (e.g. the 0x81 byte from ف, present in any path/filename with
30# Arabic/Hebrew/CJK characters). See #2056, #2099.
31#
32# No-op on macOS/Linux (already UTF-8). Must be set BEFORE Python starts —
33# changing it from inside the interpreter has no effect.
34export PYTHONUTF8=1
35
36# Git Bash / MSYS on Windows hands script paths to this shim in POSIX form
37# (`/c/Users/...`). When we exec a Windows `python.exe` (which we do on
38# Windows since `python3` is the Microsoft Store stub), python interprets the
39# leading `/` as the root of the current drive — e.g. `/c/Users/...` becomes
40# `C:\c\Users\...` or `D:\c\Users\...` (whichever drive the shell is on),
41# fails with ENOENT, and every Edit/Write/MultiEdit tool use blocks until the
42# session restarts. See anthropics/claude-plugins-official#2043.
43#
44# Fix: convert absolute path args to native Windows form via `cygpath -w`
45# before exec. `cygpath` is a Git Bash builtin; it's absent on macOS/Linux,
46# where the `command -v` guard makes this a no-op. `cygpath -w` is idempotent
47# for already-Windows paths so the rare mixed-form case is safe.
48if command -v cygpath >/dev/null 2>&1; then
49 converted=()
50 for a in "$@"; do
51 case "$a" in
52 /*) converted+=("$(cygpath -w "$a")") ;;
53 *) converted+=("$a") ;;
54 esac
55 done
56 set -- "${converted[@]}"
57fi
58
59probe() {
60 # $1..N: the interpreter command (may be multi-word like `py -3`)
61 # Writes "<major>.<minor>" to stdout and exits 0 iff at least Python 3.
62 "$@" -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")' 2>/dev/null
63}
64
65# True iff arg is a "M.m" version string >= 3.10. claude_agent_sdk requires
66# Python >= 3.10; below that, pip install fails ("No matching distribution")
67# and the LLM-powered review (Stop / commit / push) silently no-ops while
68# pattern checks (PostToolUse regex) keep working. macOS ships 3.9.6 as the
69# default `python3` on current versions, so this guard matters in practice.
70# See anthropics/claude-plugins-official#2071.
71is_sdk_compatible() {
72 case "$1" in
73 3.1[0-9]|3.[2-9][0-9]|[4-9].*|[1-9][0-9].*) return 0 ;;
74 *) return 1 ;;
75 esac
76}
77
78# Pass 1 — try minor-versioned binaries in descending order. These are only
79# present if the user explicitly installed them (Homebrew / python.org / pyenv),
80# so picking one here always upgrades over the system `python3`. Highest
81# available wins; the user doesn't have to PATH-prefer it.
82for cmd in "python3.13" "python3.12" "python3.11" "python3.10"; do
83 v=$(probe "$cmd") || continue
84 if is_sdk_compatible "$v"; then
85 exec "$cmd" "$@"
86 fi
87done
88
89# Pass 2 — bare interpreters, but only if SDK-compatible. Covers Linux distros
90# that ship 3.10+ as the default `python3`, and Windows where `python` /
91# `py -3` resolves to the user's python.org install.
92for cmd in "python3" "python" "py -3"; do
93 # shellcheck disable=SC2086
94 v=$(probe $cmd) || continue
95 if is_sdk_compatible "$v"; then
96 # shellcheck disable=SC2086
97 exec $cmd "$@"
98 fi
99done
100
101# Pass 3 — fallback to any Python 3, even <3.10. Pattern-based checks
102# (PostToolUse regex on Edit/Write) only need 3.6+ and are useful on their
103# own; the SDK-dependent paths will detect the version mismatch and degrade
104# inside the Python code. Without this fallback, the entire plugin would
105# stop working on default macOS, which is a regression vs today.
106for cmd in "python3" "python" "py -3"; do
107 # shellcheck disable=SC2086
108 v=$(probe $cmd) || continue
109 # Accept anything that successfully reported a "M.m" string.
110 case "$v" in
111 [0-9]*.[0-9]*)
112 # shellcheck disable=SC2086
113 exec $cmd "$@"
114 ;;
115 esac
116done
117
118echo "security-guidance: no working Python 3 interpreter found." >&2
119echo " tried: python3.13, python3.12, python3.11, python3.10, python3, python, py -3" >&2
120echo " on Windows, install Python from https://python.org (NOT the Microsoft Store)" >&2
121echo " on macOS, install Python 3.10+ via Homebrew (\`brew install python\`)" >&2
122exit 1