#!/usr/bin/env python3 """ HOOK: spending-guard EVENT: PreToolUse MATCHER: Bash|Write|Edit TIMEOUT: 10 PURPOSE: Blocks any action that could spend real money without human authorization. This is a HARD BLOCK -- no agent (including subagents) can bypass it. RULES ENFORCED: Autonomy Exception: "Only exception: spending real money -- ask first." WHAT IT CATCHES: - curl/wget/requests to payment/billing API endpoints (Stripe charges, Bland.ai, Vercel billing, Anthropic subscriptions, etc.) - Credit card numbers in commands or file content - Spending-intent Bash commands (stripe charge, vercel upgrade, etc.) - Attempts to read payment data from the credential vault DESIGN RATIONALE: The Bounded Autonomy Framework defines three action categories: pre-authorized (green), human-approval-required (yellow), and hard-blocked (red). Financial expenditure is the primary "yellow" category. This hook operates at the tool execution layer, not the prompt layer, ensuring the agent CANNOT spend money regardless of prompt injection, context pressure, or model confusion. AUDIT TRAIL: All blocks are logged to ~/vance/hooks-audit/spending-guard.jsonl. Critical blocks are logged with full command preview for forensic review. EXIT CODES: 0 - Always exits 0. Decision communicated via stdout JSON. """ import json import os import re import sys from datetime import datetime from pathlib import Path import _vance_bootstrap # noqa: F401 — adds VANCE_HOME to sys.path from vance_config import audit_log_path, VAULT_FILE AUDIT_LOG = audit_log_path("spending-guard") # ============================================================================= # CONFIGURATION # ============================================================================= # Payment/billing API endpoints that cost money when called BLOCKED_URL_PATTERNS = [ # Stripe -- charges, payments, subscriptions (but allow reads like list) r"api\.stripe\.com.*(charges|payment_intents|subscriptions|invoices|prices)", # Bland.ai -- block account/billing changes, allow outbound calls r"api\.bland\.ai.*(billing|account|subscription|purchase)", # Vercel billing endpoints r"api\.vercel\.com.*(billing|plan|upgrade)", # Anthropic billing r"api\.anthropic\.com.*(billing|usage|subscription)", # Twilio -- buying phone numbers costs money r"api\.twilio\.com.*IncomingPhoneNumbers", # OpenAI billing r"api\.openai\.com.*(billing|subscription)", # Stripe checkout r"checkout\.stripe\.com", # Generic purchase patterns r"buy\.|purchase\.|subscribe\.", # Cloud provider provisioning (costs money) r"api\.heroku\.com.*(dynos|addons)", r"api\.digitalocean\.com.*(droplets|databases)", r"api\.aws\.amazon\.com", # Google Cloud r"googleapis\.com.*(compute|billing)", ] # Sensitive financial data -- loaded from vault at runtime to avoid # hardcoding card numbers in source (which triggers the spending guard itself). # Falls back to empty list if vault is unavailable. def load_financial_patterns(): """Load sensitive financial patterns from the vault file. We do NOT hardcode card numbers in this script because that would cause the spending-guard hook to block writes of THIS file. Instead, we read them from the vault at enforcement time. """ try: vault_path = VAULT_FILE if vault_path.exists(): vault = json.loads(vault_path.read_text(encoding="utf-8")) patterns = [] # Extract card number from vault payment section payment = vault.get("payment", vault.get("stripe", {})) card = str(payment.get("card_number", payment.get("card", ""))) if card and len(card) >= 12: patterns.append(card) return patterns except Exception: pass return [] # Bash command patterns that suggest spending intent SPENDING_COMMAND_PATTERNS = [ r"stripe\s+(charges?|subscriptions?|payment)", r"vercel\s+.*(upgrade|\bpro\b|enterprise|billing)", r"npm\s+.*--paid", r"bland.*setup.*api.key", r"twilio.*phone.*buy", # Buying Twilio numbers r"heroku\s+(addons:create|ps:scale)", r"aws\s+(ec2|rds|s3).*create", ] # Vault payment data access patterns VAULT_PAYMENT_KEYWORDS = [ "payment", "visa", "card", "credit", "billing", ] # ============================================================================= # UTILITY FUNCTIONS # ============================================================================= def audit_log_entry(action: str, detail: str, command_preview: str = ""): """Write an entry to the audit log with command preview for forensics.""" try: AUDIT_LOG.parent.mkdir(parents=True, exist_ok=True) entry = { "timestamp": datetime.now().isoformat(), "action": action, "level": "CRITICAL", "agent": "spending-guard", "detail": detail[:500], "command_preview": command_preview[:200], } with open(AUDIT_LOG, "a", encoding="utf-8") as f: f.write(json.dumps(entry) + "\n") except Exception: pass def check_bash_command(cmd: str) -> str: """Check if a Bash command could spend money. Scans for: 1. URLs matching payment API endpoints 2. Credit card numbers in the command (loaded from vault) 3. Spending-intent command patterns 4. Vault payment data access attempts Returns a blocking reason string, or None if safe. """ # Check for payment endpoint URLs for pattern in BLOCKED_URL_PATTERNS: if re.search(pattern, cmd, re.IGNORECASE): return f"SPENDING BLOCKED: command targets payment endpoint matching '{pattern}'" # Check for credit card number in any command (loaded from vault) for pattern in load_financial_patterns(): if pattern in cmd: return "SPENDING BLOCKED: credit card number detected in command" # Check for spending-intent command patterns for pattern in SPENDING_COMMAND_PATTERNS: if re.search(pattern, cmd, re.IGNORECASE): return f"SPENDING BLOCKED: command matches spending pattern '{pattern}'" # Check for vault payment data access cmd_lower = cmd.lower() if "vance-vault" in cmd_lower: for keyword in VAULT_PAYMENT_KEYWORDS: if keyword in cmd_lower: return "SPENDING BLOCKED: attempt to access payment/card data from vault" return None def check_file_content(tool_input: dict) -> str: """Check if file content (Write/Edit) contains financial data. Returns a blocking reason string, or None if safe. """ content = tool_input.get("content", "") or tool_input.get("new_string", "") for pattern in load_financial_patterns(): if pattern in content: return "SPENDING BLOCKED: credit card number in file content" return None # ============================================================================= # MAIN ENFORCEMENT LOGIC # ============================================================================= def main(): """Spending guard entry point. Evaluates Bash commands and Write/Edit operations for patterns indicating financial expenditure. All matches result in a hard block. Human-Approval-Required Actions (Yellow): Expenditure of real money (API subscriptions, paid services, advertising spend). """ try: input_data = json.load(sys.stdin) tool_name = input_data.get("tool_name", "") tool_input = input_data.get("tool_input", {}) warning = None command_preview = "" if tool_name == "Bash": cmd = tool_input.get("command", "") command_preview = cmd[:200] warning = check_bash_command(cmd) elif tool_name in ("Write", "Edit"): warning = check_file_content(tool_input) command_preview = f"file: {tool_input.get('file_path', '')[:100]}" if warning: audit_log_entry("BLOCK", warning, command_preview) print(json.dumps({"decision": "block", "reason": warning})) sys.exit(0) print(json.dumps({})) except Exception: # Never block on hook errors -- fail open print(json.dumps({})) sys.exit(0) if __name__ == "__main__": main()