Read the script
#!/usr/bin/env bash
# PUA Integrity Guard — PreToolUse anti-cheating gate
# Separates action rights from scoring / verifier / environment-modification rights.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/flavor-helper.sh"
PUA_PY="$(pua_python_cmd 2>/dev/null || true)"
[ -n "$PUA_PY" ] || exit 0
PUA_CONFIG_PY="$(pua_to_python_path "$(pua_config_file)")"
export PUA_CONFIG_PY
TMP_INPUT=$(mktemp)
trap 'rm -f "$TMP_INPUT"' EXIT
cat > "$TMP_INPUT"
"$PUA_PY" - "$TMP_INPUT" <<'PY'
import io
import json
import os
import re
import shlex
import sys
from pathlib import Path
input_path = Path(sys.argv[1])
try:
data = json.loads(input_path.read_text(encoding='utf-8') or '{}')
except Exception:
sys.exit(0)
PUA_MARKERS = [
'PUA ACTIVATED',
'PUA Always-On',
'PUA生效',
'[PUA',
'pua:pua',
'pua-loop',
'Confidence Gate',
]
def read_text_tail(path: str, max_bytes: int = 200_000) -> str:
try:
p = Path(path).expanduser()
if not p.is_file():
return ''
size = p.stat().st_size
with p.open('rb') as f:
if size > max_bytes:
f.seek(size - max_bytes)
return f.read().decode('utf-8', errors='ignore')
except Exception:
return ''
def config_always_on() -> bool:
cfg = os.environ.get('PUA_CONFIG_PY') or os.environ.get('PUA_CONFIG') or str(Path.home() / '.pua' / 'config.json')
try:
return bool(json.loads(Path(cfg).expanduser().read_text(encoding='utf-8')).get('always_on', False))
except Exception:
return False
def is_active() -> bool:
if os.environ.get('PUA_INTEGRITY_FORCE') == '1' or os.environ.get('PUA_FORCE_ON') == '1':
return True
if config_always_on():
return True
transcript = data.get('transcript_path') or ''
text = read_text_tail(transcript)
return any(marker in text for marker in PUA_MARKERS)
if not is_active():
sys.exit(0)
tool = data.get('tool_name') or ''
tool_input = data.get('tool_input') or {}
# Keep these patterns intentionally conservative: they target governance assets,
# not ordinary application files.
PROTECTED_WRITE_PATTERNS = [
(re.compile(r'(^|/)(tests?|__tests__|test|spec|evals?|e2e|cypress|playwright)(/|$)|\.(test|spec)\.[A-Za-z0-9]+$|(^|/)(playwright|cypress)\.config\.', re.I), 'Grader gaming risk: tests/evals/E2E assets are scoring-adjacent.'),
(re.compile(r'(^|/)(score|scoring|grader|verifier)(\.[A-Za-z0-9]+)?$|(^|/)(scoring|grader|verifier)(/|$)', re.I), 'Grader gaming risk: scoring/verifier assets must not be changed by the executor.'),
(re.compile(r'(^|/)\.github/workflows(/|$)|(^|/)ci(/|$)|(^|/)(buildkite|circleci|jenkins)(/|$)', re.I), 'Environment-modification risk: CI gates are part of the verifier boundary.'),
(re.compile(r'(^|/)(feature_contracts\.json|claude-progress\.md|progress\.json|status\.json)$', re.I), 'Self-report cheating risk: status/progress files need verifier ownership.'),
(re.compile(r'(^|/)(memory|memories)(/|$)|(^|/)(decisions|failures)\.log\.jsonl$|(^|/)CLAUDE\.md$|(^|/)\.claude/(settings|settings\.local)\.json$', re.I), 'Persistent-memory risk: long-term memory/status must be append-only or approved.'),
(re.compile(r'(^|/)\.env(\.|$)|(^|/)(secrets?|credentials?)(\.|/|$)', re.I), 'Capability-abuse risk: secrets and environment files require human gate.'),
]
CONTAMINATION_PATTERNS = [
(re.compile(r'(^|/)(hidden[-_]?tests?|verifier[-_]?private|private[-_]?verifier|hidden[-_]?cases?)(/|$)', re.I), 'Solution contamination risk: hidden tests/verifier-private assets must stay outside the agent workspace.'),
(re.compile(r'(^|/)(hidden_solution|gold_patch|golden_patch|benchmark_answers?|answer_key|official_solution)(\.|/|$)', re.I), 'Solution contamination risk: hidden solution / benchmark answer artifact detected.'),
]
SENSITIVE_READ_PATTERNS = [
(re.compile(r'(^|/)\.env(\.|$)|(^|/)(secrets?|credentials?)(\.|/|$)|(^|/)(id_rsa|id_ed25519|private[-_]?key)(\.|$)', re.I), 'Capability-abuse risk: secrets and credentials require human gate.'),
]
MUTATING_BASH = re.compile(
r'(^|[;&|()\s])(rm|mv|cp|chmod|chown|truncate|tee|touch|mkdir|rmdir|git\s+(reset|clean|checkout|restore)|sed\s+(-i|--in-place)|perl\s+-p?i|python3?\s+.*open\(|node\s+.*writeFile|npm\s+version)\b|>>|>[^&]',
re.I | re.S,
)
READING_BASH = re.compile(r'(^|[;&|()\s])(cat|less|more|head|tail|sed|awk|grep|rg|find|python3?|node)\b', re.I)
WEB_CONTAMINATION = re.compile(r'(hidden[-_\s]+solution|official[-_\s]+solution|gold[-_\s]+patch|benchmark[-_\s]+answer|swe[-_\s]?bench[-_\s]+solution|leaderboard[-_\s]+answer)', re.I)
GIT_MUTATING_SUBCOMMANDS = {
'reset', 'clean', 'checkout', 'restore', 'apply', 'am', 'rm', 'mv',
}
GIT_DRY_RUN_SUBCOMMANDS = {'clean', 'rm', 'mv'}
GIT_APPLY_PREVIEW_OPTIONS = ('check', 'stat', 'numstat', 'summary')
GIT_GLOBAL_OPTIONS_WITH_VALUE = {
'-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path',
'--super-prefix', '--config-env',
}
GIT_GLOBAL_OPTIONS_WITH_ATTACHED_VALUE = (
'-C', '-c', '--git-dir=', '--work-tree=', '--namespace=', '--exec-path=',
'--super-prefix=', '--config-env=',
)
GIT_PATHSPEC_MAGIC = re.compile(r'(^:|[\*\?\[\]\{\}\$])')
def command_tokens(command: str):
try:
return shlex.split(command)
except Exception:
return re.split(r'\s+', command)
def is_direct_git_command(tokens) -> bool:
if not tokens:
return False
executable = tokens[0].replace('\\', '/').rsplit('/', 1)[-1].lower()
return executable in {'git', 'git.exe'}
def git_subcommand_and_args(tokens):
"""Return a direct Git subcommand and its arguments, if one is present."""
if not is_direct_git_command(tokens):
return None
arg_index = 1
while arg_index < len(tokens):
arg = tokens[arg_index]
if arg == '--':
return None
if arg in {'-h', '--help', '--version'}:
return None
if ar