From 7d6aa3ac65f48e7e4c6c69faff50937c820158b7 Mon Sep 17 00:00:00 2001 From: Julius Brussee Date: Thu, 9 Apr 2026 01:07:16 +0200 Subject: [PATCH] Merge PR #38: Security patch, SDK support, and file size limit --- .gitignore | 1 + README.md | 6 +++ caveman-compress/README.md | 16 ++++--- caveman-compress/SECURITY.md | 31 +++++++++++++ caveman-compress/scripts/benchmark.py | 4 +- caveman-compress/scripts/cli.py | 4 +- caveman-compress/scripts/compress.py | 67 +++++++++++++-------------- caveman-compress/scripts/detect.py | 2 +- caveman-compress/scripts/validate.py | 4 +- 9 files changed, 88 insertions(+), 47 deletions(-) create mode 100644 caveman-compress/SECURITY.md diff --git a/.gitignore b/.gitignore index 6e645b3..8afb411 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ .env.local caveman-compress.md **/.DS_Store +.claude/worktrees/ diff --git a/README.md b/README.md index 3f05aa5..d8f2c5f 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,12 @@ Compress is built in with the caveman plugin — no separate install needed. - Use `/caveman` for caveman mode - Use `/caveman:compress ` for memory-file compression +## Security + +`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./caveman-compress/SECURITY.md) for a full explanation of what the skill does and does not do. + +See the full [caveman-compress README](caveman-compress/README.md) for install, usage, and validation details. + ## Star This Repo If caveman save you mass token, mass money — leave mass star. ⭐ diff --git a/caveman-compress/README.md b/caveman-compress/README.md index 34984ef..3199c99 100644 --- a/caveman-compress/README.md +++ b/caveman-compress/README.md @@ -65,6 +65,10 @@ All validations passed ✅ — headings, code blocks, URLs, file paths preserved **Same instructions. 60% fewer tokens. Every. Single. Session.** +## Security + +`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./SECURITY.md) for a full explanation of what the skill does and does not do. + ## Install Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman:compress`. @@ -143,12 +147,12 @@ Caveman compress natural language. It never touch: Caveman cut that by ~45% on average. Same instructions. Same accuracy. Less waste. ``` -┌──────────────────────────────────────────┐ -│ TOKEN SAVINGS PER FILE ████████ 45% │ -│ SESSIONS THAT BENEFIT ████████ 100% │ -│ INFORMATION PRESERVED ████████ 100% │ -│ SETUP TIME █ 1x │ -└──────────────────────────────────────────┘ +┌────────────────────────────────────────────┐ +│ TOKEN SAVINGS PER FILE █████ 45% │ +│ SESSIONS THAT BENEFIT ██████████ 100% │ +│ INFORMATION PRESERVED ██████████ 100% │ +│ SETUP TIME █ 1x │ +└────────────────────────────────────────────┘ ``` ## Part of Caveman diff --git a/caveman-compress/SECURITY.md b/caveman-compress/SECURITY.md new file mode 100644 index 0000000..693108c --- /dev/null +++ b/caveman-compress/SECURITY.md @@ -0,0 +1,31 @@ +# Security + +## Snyk High Risk Rating + +`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do. + +### What triggers the rating + +1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. The subprocess call uses a fixed argument list — no shell interpolation occurs. User file content is passed via stdin, not as a shell argument. + +2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved alongside it. No files outside the user-specified path are read or written. + +### What the skill does NOT do + +- Does not execute user file content as code +- Does not make network requests except to Anthropic's API (via SDK or CLI) +- Does not access files outside the path the user provides +- Does not use shell=True or string interpolation in subprocess calls +- Does not collect or transmit any data beyond the file being compressed + +### Auth behavior + +If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication. + +### File size limit + +Files larger than 500KB are rejected before any API call is made. + +### Reporting a vulnerability + +If you believe you've found a genuine security issue, please open a GitHub issue with the label `security`. diff --git a/caveman-compress/scripts/benchmark.py b/caveman-compress/scripts/benchmark.py index 40deab4..5b8f7ed 100644 --- a/caveman-compress/scripts/benchmark.py +++ b/caveman-compress/scripts/benchmark.py @@ -44,8 +44,8 @@ def print_table(rows): def main(): # Direct file pair: python3 benchmark.py original.md compressed.md if len(sys.argv) == 3: - orig = Path(sys.argv[1]) - comp = Path(sys.argv[2]) + orig = Path(sys.argv[1]).resolve() + comp = Path(sys.argv[2]).resolve() if not orig.exists(): print(f"❌ Not found: {orig}") sys.exit(1) diff --git a/caveman-compress/scripts/cli.py b/caveman-compress/scripts/cli.py index d2925d3..428fd86 100644 --- a/caveman-compress/scripts/cli.py +++ b/caveman-compress/scripts/cli.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Caveman Memory CLI +Caveman Compress CLI Usage: caveman @@ -33,6 +33,8 @@ def main(): print(f"❌ Not a file: {filepath}") sys.exit(1) + filepath = filepath.resolve() + # Detect file type file_type = detect_file_type(filepath) diff --git a/caveman-compress/scripts/compress.py b/caveman-compress/scripts/compress.py index 977a4ea..361443b 100644 --- a/caveman-compress/scripts/compress.py +++ b/caveman-compress/scripts/compress.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 """ -Caveman Memory Orchestrator +Caveman Memory Compression Orchestrator Usage: - python memory/compress.py + python scripts/compress.py """ +import os import subprocess -import sys from pathlib import Path from typing import List @@ -21,6 +21,21 @@ MAX_RETRIES = 2 def call_claude(prompt: str) -> str: + api_key = os.environ.get("ANTHROPIC_API_KEY") + if api_key: + try: + import anthropic + + client = anthropic.Anthropic(api_key=api_key) + msg = client.messages.create( + model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"), + max_tokens=8096, + messages=[{"role": "user", "content": prompt}], + ) + return msg.content[0].text.strip() + except ImportError: + pass # anthropic not installed, fall back to CLI + # Fallback: use claude CLI (handles desktop auth) try: result = subprocess.run( ["claude", "--print"], @@ -85,10 +100,18 @@ Return ONLY the fixed compressed file. No explanation. def compress_file(filepath: Path) -> bool: - print(f"📄 Processing: {filepath}") + # Resolve and validate path + filepath = filepath.resolve() + MAX_FILE_SIZE = 500_000 # 500KB + if not filepath.exists(): + raise FileNotFoundError(f"File not found: {filepath}") + if filepath.stat().st_size > MAX_FILE_SIZE: + raise ValueError(f"File too large to compress safely (max 500KB): {filepath}") + + print(f"Processing: {filepath}") if not should_compress(filepath): - print("⚠️ Skipping (not natural language)") + print("Skipping (not natural language)") return False original_text = filepath.read_text(errors="ignore") @@ -102,7 +125,7 @@ def compress_file(filepath: Path) -> bool: return False # Step 1: Compress - print("🧠 Compressing with Claude...") + print("Compressing with Claude...") compressed = call_claude(build_compress_prompt(original_text)) # Save original as backup, write compressed to original path @@ -111,12 +134,12 @@ def compress_file(filepath: Path) -> bool: # Step 2: Validate + Retry for attempt in range(MAX_RETRIES): - print(f"\n🔍 Validation attempt {attempt + 1}") + print(f"\nValidation attempt {attempt + 1}") result = validate(backup_path, filepath) if result.is_valid: - print("✅ Validation passed") + print("Validation passed") break print("❌ Validation failed:") @@ -130,36 +153,10 @@ def compress_file(filepath: Path) -> bool: print("❌ Failed after retries — original restored") return False - print("🛠 Fixing with Claude...") + print("Fixing with Claude...") compressed = call_claude( build_fix_prompt(original_text, compressed, result.errors) ) filepath.write_text(compressed) return True - - -# ---------- Main ---------- - - -def main(): - if len(sys.argv) != 2: - print("Usage: python memory/compress.py ") - sys.exit(1) - - filepath = Path(sys.argv[1]) - - if not filepath.exists(): - print(f"❌ File not found: {filepath}") - sys.exit(1) - - success = compress_file(filepath) - - if success: - sys.exit(0) - else: - sys.exit(2) - - -if __name__ == "__main__": - main() diff --git a/caveman-compress/scripts/detect.py b/caveman-compress/scripts/detect.py index bd2daf4..5f50fd3 100644 --- a/caveman-compress/scripts/detect.py +++ b/caveman-compress/scripts/detect.py @@ -115,7 +115,7 @@ if __name__ == "__main__": sys.exit(1) for path_str in sys.argv[1:]: - p = Path(path_str) + p = Path(path_str).resolve() file_type = detect_file_type(p) compress = should_compress(p) print(f" {p.name:30s} type={file_type:20s} compress={compress}") diff --git a/caveman-compress/scripts/validate.py b/caveman-compress/scripts/validate.py index e9fff3f..d28f1f8 100644 --- a/caveman-compress/scripts/validate.py +++ b/caveman-compress/scripts/validate.py @@ -131,8 +131,8 @@ if __name__ == "__main__": print("Usage: python validate.py ") sys.exit(1) - orig = Path(sys.argv[1]) - comp = Path(sys.argv[2]) + orig = Path(sys.argv[1]).resolve() + comp = Path(sys.argv[2]).resolve() res = validate(orig, comp)