Read the script
#!/usr/bin/env python3
"""
Hardstop Plugin — PreToolUse Hook (Read)
Blocks reading of sensitive credential files to prevent secrets exposure.
Exit codes:
0 = Success (uses JSON output for allow/deny decision)
Blocking uses permissionDecision: "deny" in JSON output instead of exit code 2.
This ensures consistent behavior between CLI and VS Code extension.
Design principle: Fail-closed. If safety check fails, block the read.
"""
import sys
import json
import re
import os
from pathlib import Path
from datetime import datetime
from typing import Tuple, Optional, List, Dict
# Import pattern loader for YAML-based patterns
try:
from pattern_loader import load_dangerous_reads, load_sensitive_reads
PATTERN_LOADER_AVAILABLE = True
except ImportError:
PATTERN_LOADER_AVAILABLE = False
# Import session tracker for risk scoring (v1.4.0+)
try:
from session_tracker import get_tracker
SESSION_TRACKER_AVAILABLE = True
except ImportError:
SESSION_TRACKER_AVAILABLE = False
# === CONFIGURATION ===
STATE_DIR = Path.home() / ".hardstop"
SKIP_FILE = STATE_DIR / "skip_next"
LOG_FILE = STATE_DIR / "audit.log"
DEBUG_FILE = STATE_DIR / "hook_debug.log"
# Fail-closed: if True, errors during safety check block the read
FAIL_CLOSED = True
# === DEBUG LOGGING ===
try:
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(DEBUG_FILE, "a") as f:
f.write(f"[{datetime.now().isoformat()}] Read hook invoked\n")
except:
pass
# === DANGEROUS READ PATTERNS ===
# These paths contain secrets that should never be read by AI
# Pattern registries: store full pattern metadata for risk scoring
_READ_PATTERN_REGISTRY: Dict[str, Dict] = {}
_SENSITIVE_READ_REGISTRY: Dict[str, Dict] = {}
# Load patterns from YAML files (v1.4.0+) or use fallback
def _load_dangerous_read_patterns() -> List[Tuple[str, str]]:
"""
Load dangerous read patterns from YAML files.
Returns list of (regex, pattern_id) tuples.
Builds _READ_PATTERN_REGISTRY with full metadata.
Falls back to empty list if pattern loader unavailable.
"""
global _READ_PATTERN_REGISTRY
if not PATTERN_LOADER_AVAILABLE:
print("Warning: pattern_loader not available, using empty dangerous read patterns list", file=sys.stderr)
return []
try:
yaml_patterns = load_dangerous_reads()
# Build registry with full pattern data
_READ_PATTERN_REGISTRY.clear()
for pattern in yaml_patterns:
pattern_id = pattern.get('id', 'UNKNOWN')
_READ_PATTERN_REGISTRY[pattern_id] = pattern
# Return matching list: (regex, pattern_id)
return [(p['regex'], p.get('id', 'UNKNOWN')) for p in yaml_patterns
if 'regex' in p]
except Exception as e:
print(f"Warning: Failed to load dangerous read patterns: {e}", file=sys.stderr)
return []
DANGEROUS_READ_PATTERNS = _load_dangerous_read_patterns()
def _load_sensitive_read_patterns() -> List[Tuple[str, str]]:
"""
Load sensitive read patterns from YAML files.
Returns list of (regex, pattern_id) tuples.
Builds _SENSITIVE_READ_REGISTRY with full metadata.
Falls back to empty list if pattern loader unavailable.
"""
global _SENSITIVE_READ_REGISTRY
if not PATTERN_LOADER_AVAILABLE:
print("Warning: pattern_loader not available, using empty sensitive read patterns list", file=sys.stderr)
return []
try:
yaml_patterns = load_sensitive_reads()
# Build registry with full pattern data
_SENSITIVE_READ_REGISTRY.clear()
for pattern in yaml_patterns:
pattern_id = pattern.get('id', 'UNKNOWN')
_SENSITIVE_READ_REGISTRY[pattern_id] = pattern
# Return matching list: (regex, pattern_id)
return [(p['regex'], p.get('id', 'UNKNOWN')) for p in yaml_patterns
if 'regex' in p]
except Exception as e:
print(f"Warning: Failed to load sensitive read patterns: {e}", file=sys.stderr)
return []
SENSITIVE_READ_PATTERNS = _load_sensitive_read_patterns()
# Note: Legacy hardcoded patterns removed in v1.4.0 (now in patterns/sensitive_reads.yaml)
# === SAFE READ PATTERNS ===
# Explicit allowlist for common safe reads
SAFE_READ_PATTERNS = [
# Documentation
r"README\.md$",
r"README\.rst$",
r"README\.txt$",
r"README$",
r"CHANGELOG\.md$",
r"CHANGELOG$",
r"HISTORY\.md$",
r"LICENSE$",
r"LICENSE\.md$",
r"LICENSE\.txt$",
r"CONTRIBUTING\.md$",
r"CODE_OF_CONDUCT\.md$",
r"\.md$",
r"\.rst$",
r"\.txt$",
# Source code
r"\.py$",
r"\.pyi$",
r"\.js$",
r"\.mjs$",
r"\.cjs$",
r"\.ts$",
r"\.tsx$",
r"\.jsx$",
r"\.go$",
r"\.rs$",
r"\.java$",
r"\.kt$",
r"\.scala$",
r"\.c$",
r"\.cpp$",
r"\.cc$",
r"\.h$",
r"\.hpp$",
r"\.cs$",
r"\.rb$",
r"\.php$",
r"\.swift$",
r"\.m$",
r"\.mm$",
r"\.lua$",
r"\.pl$",
r"\.sh$",
r"\.bash$",
r"\.zsh$",
r"\.fish$",
r"\.ps1$",
r"\.bat$",
r"\.cmd$",
r"\.sql$",
r"\.graphql$",
r"\.gql$",
# Config (Non-Sensitive)
r"package\.json$",
r"package-lock\.json$",
r"yarn\.lock$",
r"pnpm-lock\.yaml$",
r"tsconfig\.json$",
r"jsconfig\.json$",
r"pyproject\.toml$",
r"setup\.py$",
r"setup\.cfg$",
r"Cargo\.toml$",
r"Cargo\.lock$",
r"go\.mod$",
r"go\.sum$",
r"requirements\.txt$",
r"Pipfile$",
r"Pipfile\.lock$",
r"Gemfile$",
r"Gemfile\.lock$",
r"composer\.json$",
r"composer\.lock$",
r"Makefile$",
r"CMakeLists\.txt$",
r"\.gitignore$",
r"\.dockerignore$",
r"Dockerfile$",
r"docker-compose\.yml$",
r"docker-compose\.yaml$",
# Example/Template Files (safe versions of .env)
r"\.env\.example$",
r"\.env\.template$",
r"\.env\.sample$",
r"\.env\.dist$",
r"example\.",
r"sample\.",
r"template\.",
# Web assets