Merge PR #38: Security patch, SDK support, and file size limit

This commit is contained in:
Julius Brussee
2026-04-09 01:07:16 +02:00
parent e1e5e8b13a
commit 7d6aa3ac65
9 changed files with 88 additions and 47 deletions
+1
View File
@@ -5,3 +5,4 @@ __pycache__/
.env.local
caveman-compress.md
**/.DS_Store
.claude/worktrees/
+6
View File
@@ -275,6 +275,12 @@ Compress is built in with the caveman plugin — no separate install needed.
- Use `/caveman` for caveman mode
- Use `/caveman:compress <filepath>` 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. ⭐
+10 -6
View File
@@ -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
+31
View File
@@ -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`.
+2 -2
View File
@@ -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)
+3 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Caveman Memory CLI
Caveman Compress CLI
Usage:
caveman <filepath>
@@ -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)
+32 -35
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env python3
"""
Caveman Memory Orchestrator
Caveman Memory Compression Orchestrator
Usage:
python memory/compress.py <filepath>
python scripts/compress.py <filepath>
"""
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 <filepath>")
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()
+1 -1
View File
@@ -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}")
+2 -2
View File
@@ -131,8 +131,8 @@ if __name__ == "__main__":
print("Usage: python validate.py <original> <compressed>")
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)